Jeder Scraper braucht Retry-Logik, und bei den meisten wächst sie zufällig: hier ein try-Block, dort ein time.sleep, ein Versuchszähler, der in der Woche hinzugefügt wurde, als etwas kaputtging. Das Ergebnis funktioniert bis zu dem Tag, an dem ein Ziel eine schlechte Stunde hat, und an diesem Punkt richtet der Retry-Pfad mehr Schaden an als der ursprüngliche Fehler. Das Argument dafür, warum das passiert, steht in retry and backoff; dies ist die Umsetzung.
Die ordnende Idee ist, dass ein Retry eine Entscheidung ist, die aus einem klassifizierten Fehler getroffen wird, nicht eine Schleife, die um eine Anfrage gewickelt wird. Wenn die Klassifizierung stimmt, folgt der Rest.
Erst klassifizieren
Jeder Fehler fällt in eine von vier Klassen, und jede hat genau eine korrekte Antwort.
| Klasse | Beispiele | Antwort |
|---|---|---|
| Transient | Timeout, Connection Reset, 502, 503, 504 | Retry mit Backoff |
| Rate | 429, Retry-After vorhanden | wie angewiesen warten, gleiche Route |
| Identity | Challenge-Seite, anhaltender 403, Block-Seite | neue Session, dann Retry |
| Terminal | 404, 400, 401, 407, Parse-Fehler | nicht erneut versuchen, weitergeben |
Bei zweien davon liegen die Leute typischerweise falsch. Rate-Signale sind Anweisungen zum Tempo, daher besteht die Antwort darin zu warten statt Adressen zu wechseln, denn Rotation, um das gleiche Tempo aufrechtzuerhalten, ist genau das Verhalten, das eine Drosselung zu einer Sperre eskalieren lässt. Terminal-Fehler dürfen niemals wiederholt werden: Ein 407 bedeutet fehlerhafte Zugangsdaten oder ein falsch formatiertes Username-Flag und ist beim fünften Versuch genauso falsch, und ein Parse-Fehler ist ein Bug in Ihrem Code, den ein Retry treu und für immer reproduziert.
Der fünfte Fall ist derjenige, der niemals überhaupt in einem Statuscode auftaucht: eine Antwort, die in Ordnung aussieht und es nicht ist. Eine Challenge-Seite, eine leere Ergebnismenge oder eine abgeschnittene Liste, ausgeliefert mit einem 200, gehört in die Identity-Klasse, aber nur, wenn Sie es bemerken, was bedeutet, den Body zu validieren, bevor Sie irgendetwas entscheiden. Diese Prüfung ist das Fundament des gesamten Systems, gemäß detecting blocked or fake content.
Klassifizierung im Code
import random, time, requests
TRANSIENT = {502, 503, 504}
def classify(resp, exc, is_valid):
if exc is not None:
return "transient" # timeout, reset, DNS
if resp.status_code == 429:
return "rate"
if resp.status_code in TRANSIENT:
return "transient"
if resp.status_code in (403, 401) or looks_like_challenge(resp):
return "identity"
if resp.status_code == 200 and not is_valid(resp.text):
return "identity" # soft block: 200 but not our data
if resp.ok:
return "ok"
return "terminal" # 404, 400, 407, everything else
looks_like_challenge ist zielspezifisch und meist eine kurze Liste von Markern: ein Captcha-Skriptverweis, ein bekannter Interstitial-Titel, ein Body, der weit kürzer ist als eine echte Seite. Halten Sie das pro Ziel an einer Stelle, damit sowohl der Retry-Pfad als auch Ihr Monitoring dieselbe Definition verwenden.
Backoff mit Jitter, und Einhaltung von Retry-After
Für die Transient-Klasse wächst die Verzögerung exponentiell und muss randomisiert sein. Feste Verzögerungen synchronisieren Ihre Worker, sodass hundert Anfragen, die gemeinsam fehlschlagen, gemeinsam erneut versucht werden, und der Burst den Backoff übersteht.
def backoff(attempt, base=1.0, cap=60.0):
ceiling = min(cap, base * (2 ** attempt))
return random.uniform(0, ceiling) # full jitter
Für die Rate-Klasse sagt Ihnen das Ziel möglicherweise genau, wie lange Sie warten sollen, und diese Anweisung schlägt Ihren eigenen Zeitplan:
def wait_for(resp, attempt):
ra = resp.headers.get("Retry-After")
if ra:
try:
return min(float(ra), 300) # honour it, but cap it
except ValueError:
pass # HTTP-date form, fall through
return backoff(attempt, base=2.0) # rate signals start slower
Alles zusammenfügen
MAX_ATTEMPTS = 4
def fetch(url, country, is_valid, session_id=None):
sid = session_id
for attempt in range(MAX_ATTEMPTS):
proxies = build_proxies(country, sid) # sid=None means rotate
resp = exc = None
try:
resp = requests.get(url, proxies=proxies, timeout=20)
except requests.RequestException as e:
exc = e
kind = classify(resp, exc, is_valid)
if kind == "ok":
return resp
if kind == "terminal":
raise TerminalError(url, getattr(resp, "status_code", None))
if kind == "identity":
sid = new_session_id() if sid else None # retire the session
time.sleep(backoff(attempt))
elif kind == "rate":
time.sleep(wait_for(resp, attempt)) # wait, do not rotate
else:
time.sleep(backoff(attempt))
raise Exhausted(url)
Drei Details darin sind wichtiger als die Struktur. Die Funktion wirft bei terminalen Fehlern statt None zurückzugeben, sodass ein Aufrufer einen Fehler nicht mit leeren Daten verwechseln kann. Identity-Fehler ersetzen die Session statt sie wiederzuverwenden, weil die vorherige dem Ziel bereits bekannt ist. Und Rate-Fehler rühren die Session bewusst nicht an, wodurch die gleiche Route beibehalten wird, während verlangsamt wird.
Leitplanken, die verhindern, dass Retries selbst zum Problem werden
Logik pro Anfrage allein reicht nicht aus, weil ihr der Blick auf das System fehlt. Drei Ergänzungen leisten den Großteil der Schutzarbeit.
Ein Retry-Budget begrenzt Retries als Anteil am Gesamtverkehr zu einem Ziel, sagen wir zehn Prozent. Im Normalbetrieb kommen Sie diesem Wert nie nahe. Wenn ein Ziel großflächig ausfällt, ist das Budget sofort erschöpft und die zusätzlichen Retries finden schlicht nicht statt, was das gewünschte Verhalten ist, denn Retries helfen bei isolierten Fehlern und schaden aktiv während eines allgemeinen Ausfalls.
Ein Circuit Breaker pro Ziel stoppt das Senden vollständig, sobald die Fehlerrate eine Schwelle überschreitet, wartet eine Abkühlphase ab und lässt dann ein Rinnsal durch, um die Erholung zu testen. Das schützt das Ziel vor Ihrem Ansturm und schützt Ihre Adressen davor, Fehler gegen eine Seite anzuhäufen, die niemandem antwortet.
Ein gemeinsamer Rate Limiter, den auch Retries durchlaufen. Wenn Retries Ihr Tempo umgehen, wird Ihr Fehlerpfad zu einer ungedrosselten Flut, genau dann, wenn das Ziel am wenigsten in der Lage ist, damit umzugehen. Leiten Sie jeden Versuch durch denselben Limiter, gemäß rate limiting and throttling.
Alle drei gehören in die Komponente, die bereits jede Anfrage sieht, was das Argument für einen Proxy Manager ist, statt Retry-Code pro Job.
Idempotenz und die Dead-Letter-Queue
Zwei praktische Belange, die leicht vergessen werden.
Retries setzen voraus, dass der Vorgang gefahrlos wiederholt werden kann. Für das Sammeln von Daten trifft das fast immer zu, da das zweimalige Abrufen einer Seite nur Bandbreite kostet und sonst nichts. Wenn irgendein Teil Ihrer Pipeline als Nebeneffekt des Abrufens schreibt, machen Sie das Schreiben idempotent, verankert an etwas Stabilem, damit ein Retry keinen doppelten Datensatz erzeugt.
Und wenn eine Anfrage ihre Versuche erschöpft, verwerfen Sie sie nicht. Schieben Sie sie in eine Dead-Letter-Queue und verarbeiten Sie sie viel später erneut, im nächsten Lauf oder nach einer langen Abkühlphase, statt innerhalb des aktuellen Bursts. Die meisten Elemente, die während eines Vorfalls fehlschlagen, gelingen eine Stunde später von selbst, und eine Queue macht aus einem harten Fehler einen aufgeschobenen, ohne Kosten.
Die Retry-Rate im Blick behalten
Retries sind die früheste Warnung, die Sie bekommen. Die Retry-Quote, also Retries als Anteil der Anfragen pro Ziel, steigt, bevor die Erfolgsquote fällt, denn eine Pipeline, die sich zu einem normal aussehenden Ergebnis durchretried, verbirgt ein Problem statt es zu lösen. Sie ist zudem reine Kosten bei einem nach Bandbreite abgerechneten Produkt, also gleichzeitig eine Gesundheits- und eine Ausgabenmetrik. Platzieren Sie sie im Dashboard neben der validierten Erfolgsquote, gemäß proxy KPIs und monitoring your pipeline.
Fazit
Retry-Logik ist ein Klassifikator gefolgt von vier Antworten, keine Schleife mit einem Zähler. Validieren Sie den Body, damit Soft Blocks als Fehler klassifiziert werden, warten Sie dann bei Rate-Signalen ohne zu rotieren, ziehen Sie die Session bei Identity-Fehlern zurück, machen Sie bei transienten Fehlern Backoff mit Jitter, und versuchen Sie einen terminalen Fehler nie erneut. Begrenzen Sie Versuche und Verzögerung. Fügen Sie dann die Leitplanken hinzu, die eine Sicht pro Anfrage nicht liefern kann: ein Retry-Budget, damit ein großflächiger Ausfall nicht zu einer Flut werden kann, einen Circuit Breaker pro Ziel und einen gemeinsamen Limiter, den auch Retries respektieren. Schicken Sie erschöpfte Anfragen in eine Dead-Letter-Queue für einen späteren Lauf, und beobachten Sie die Retry-Quote als Ihr frühestes Signal dafür, dass sich etwas ändert.
Die Failover-Hälfte davon, nämlich irgendwo Sauberes zu haben, wohin man erneut versuchen kann, ist das, was residential proxies bieten: ein großer Pool echter, heimischer Adressen, sodass eine zurückgezogene Session ersetzt statt wiederverwendet wird, mit per-GB pricing, das disziplinierte Retries direkt günstiger macht als undisziplinierte.