So unterscheiden Sie in Python-Scrapern zwischen Captcha und Sperre

Wenn ein Python-Scraper eine schlechte Antwort bekommt, lautet die erste Frage: Captcha oder Sperre, denn die Antwort entscheidet, was als Nächstes zu tun ist. Ein Captcha lässt sich lösen. Ein Rate-Limit braucht eine Wartezeit. Eine Sperrseite bedeutet, dass Ihre Adresse oder Ihr Fingerabdruck abgelehnt wird, und kein Token ändert daran etwas. Dieser Leitfaden baut einen Klassifikator von etwa 40 Zeilen, der Status, Header und Markup liest und eines von sechs Labels zurückgibt, dann eine Abruf-Schleife, die CapSkip nur für das Label aufruft, bei dem ein Lösevorgang helfen kann. CapSkip läuft auf Ihrem eigenen Windows-PC ohne Gebühr pro Lösung, daher spart das Überspringen eines Lösevorgangs, der nicht funktionieren kann, Zeit, nicht Geld.
Was Sie brauchen
- CapSkip läuft auf einem Windows-PC, mit eingeschaltetem reCAPTCHA-Löser. Die Einrichtungsanleitung führt durch den ersten Start.
- Python 3.10 oder neuer. Der Code unten lief auf Python 3.14.0 mit requests 2.33.0 und capskip 1.3.0.
- Eine PowerShell-Eingabeaufforderung. Die Befehle verwenden PowerShell-Syntax.
- Seiten, die Ihnen gehören oder die Sie automatisieren dürfen.
- Eine Adresse für den Solver. Im Local-Modus antwortet CapSkip auf 127.0.0.1 nur für dieses Gerät. Im Server-Modus lauscht es auf Ihrer Netzwerkadresse oder öffentlichen IP, damit ein Scraper auf einem anderen PC, einem VPS oder einer gehosteten Plattform es über die API aufrufen kann. Eingestellt werden beide Modi in den Verbindungseinstellungen.
So wurde getestet: Der Klassifikator und die Abruf-Schleife bestanden 17 Prüfungen auf Python 3.14.0, jede davon eine Antwort, die eine lokale Fixture-Website auslieferte. Die Lösevorgänge liefen über das echte capskip-Paket gegen einen Mock-CapSkip (einen nachgebauten Testersatz), nicht gegen die App. Zwei öffentliche Seiten hinter der Vollseiten-Prüfung von Cloudflare wurden am 29. Oktober 2026 ebenfalls abgerufen, und beide gaben eine 403 mit einem Header cf-mitigated zurück. Keine Zahl unten ist eine Lösezeit.
Warum Captcha oder Sperre die erste Frage ist
Die beiden Fehler kosten Unterschiedliches. Eine Sperrseite an einen Solver zu schicken, verschwendet den Lösevorgang, und Sie bleiben gesperrt. Jedes Mal zu einem neuen Proxy zu wechseln, wenn ein Captcha erscheint, verbrennt Adressen, die Sie nicht hätten verbrennen müssen. Eine Seite, die ein Captcha ausliefert, verlangt meist, dass Sie etwas beweisen, und ein gelöster Token lässt Sie durch. Eine Seite, die eine Sperre ausliefert, weist Sie rundheraus ab. Die HTTP-429-Anleitung behandelt den Teil mit dem Warten für einen einzelnen Statuscode. Dieser Leitfaden behandelt die Entscheidung, die davor kommt.
Sechs Labels decken alles ab: ok, rate_limited, captcha, cloudflare_challenge, blocked und unknown. Das Label unknown ist wichtig. Ein Klassifikator, der unter den übrigen fünf wählen muss, wird manchmal falsch wählen, und eine Schleife, die bei unknown eine Exception auslöst, zeigt Ihnen den Fall, den Sie noch nicht behandelt haben.
Schritt 1: Installieren
Legen Sie in einem leeren Ordner eine virtuelle Umgebung an und installieren Sie die zwei Pakete. Die Befehle rufen das Python der Umgebung direkt über den Pfad auf, Sie müssen sie also nicht aktivieren.
py -m venv .venv .\.venv\Scripts\python.exe -m pip install capskip requests
Schritt 2: die Signale
Speichern Sie diesen Block und den nächsten als classify.py. Der erste Block enthält die Muster. Das Widget-Muster sucht ein Tag, dessen Attribut class ein Widget nennt, oder ein Tag altcha-widget. Ein Blogbeitrag, der das Markup in maskiertem Code zeigt, ein Link, dessen Adresse einen Widget-Namen enthält, und ein verstecktes Eingabefeld g-recaptcha-response passen alle nicht.
Die letzte Zeile enthält Formulierungen, die eine weiche Sperre erkennen lassen, also eine Seite, die einen normalen Status zurückgibt, aber sagt, dass Sie abgelehnt werden. Seien Sie damit vorsichtig. Die Fehlerdokumentation von Cloudflare führt Error 1020: Access denied auf, woher die ersten beiden Formulierungen stammen. Die anderen beiden sind Vermutungen aus Sperrseiten in freier Wildbahn, und Ihr Ziel hat seine eigene Wortwahl. Ersetzen Sie sie, nachdem Sie ein paar echte gesehen haben.
import re
# A tag whose class attribute names a widget, or an altcha-widget tag. A link, a hidden
# input or a sentence that merely contains the name does not match.
CLS = r"<[^>]*\bclass\s*=\s*[\"'][^\"']*(?<![\w-])%s(?![\w-])"
WIDGET = re.compile(CLS % "(?:g-recaptcha|h-captcha|cf-turnstile|frc-captcha|captchafox)"
+ r"|<altcha-widget\b", re.I)
TITLE = re.compile(r"<title[^>]*>(.*?)</title>", re.I | re.S)
BLOCK = ("access denied", "error 1020", "unusual traffic", "you have been blocked")Schritt 3: Captcha oder Sperre entscheiden
Die Reihenfolge der Prüfungen ist die Logik. Jede Regel läuft nur, wenn die darüber nicht gepasst haben.
- Der Header cf-mitigated kommt zuerst. Cloudflare dokumentiert den Wert challenge als den, den es auf Challenge-Seiten sendet, und er ist das stärkste Signal in der Funktion.
- Als Nächstes wird eine Weiterleitung geprüft, für Aufrufer, die das Folgen von Weiterleitungen abschalten. Sie heißt unknown, weil die Adresse allein nicht sagen kann, ob sie zu einem Captcha oder zu einem Artikel mit dem Wort im Namen führt.
- Die Skriptvariable _cf_chl_opt und der Titel Just a moment kommen danach. Sie erkennen eine Challenge-Seite, wenn ein Proxy oder ein CDN den Header entfernt hat.
- Mit dem Argument expect nennen Sie eine Zeichenfolge, die nur die echte Seite enthält. Ist sie vorhanden, heißt die Seite ok, egal welcher Status. Ohne sie würde eine Inhaltsseite mit einem Captcha-Widget in der Fußzeile als Captcha bezeichnet.
- Ein Widget im Markup bedeutet captcha, auch bei einer 429 oder 403. Das läuft vor den Statusregeln, weil manche Websites ihr Captcha mit einem Fehlerstatus ausliefern. Die Widget-Liste deckt reCAPTCHA, hCaptcha, Turnstile, Friendly Captcha, CaptchaFox und ALTCHA ab.
- Eine 429 oder eine 503 mit Retry-After bedeutet rate_limited. Eine 403 mit HTML-Body bedeutet blocked, es sei denn, der Seitentext enthält csrf. Eine JSON-403 bleibt unknown, denn eine API, die sagt, Ihrem Token fehle ein Scope, sagt Ihnen nicht, dass Sie den Proxy wechseln sollen.
- Eine Antwort mit einem Status unter 400 ist eine weiche Sperre, wenn ihr Titel eine der Formulierungen enthält oder wenn die ganze Seite kürzer als 3000 Zeichen ist und eine enthält.
def classify(resp, expect=None):
"""expect: a string only the real page contains, for pages that carry a widget anyway."""
code, h = resp.status_code, resp.headers
if h.get("cf-mitigated", "").lower() == "challenge":
return "cloudflare_challenge" # Cloudflare docs
if 300 <= code < 400: # allow_redirects=False callers
return "unknown"
html = "html" in h.get("content-type", "").lower()
text = resp.text[:200000] if html else ""
low = text.lower()
if "_cf_chl_opt" in text or "<title>just a moment" in low:
return "cloudflare_challenge" # header stripped by a proxy
if expect and expect in text:
return "ok"
if WIDGET.search(text):
return "captcha" # even on a 429 or a 403
if code == 429 or (code == 503 and "retry-after" in h):
return "rate_limited"
if code == 403 and html and "csrf" not in low:
return "blocked" # JSON 403s fall through
m = TITLE.search(text)
head = m.group(1).lower() if m else ""
small = len(text) < 3000 # block pages are short
if code < 400 and html and any(b in head or (small and b in low) for b in BLOCK):
return "blocked" # soft block: 200 with a notice
return "ok" if code < 400 else "unknown"Schritt 4: Retry-After lesen
Speichern Sie diesen Block, den nächsten und den danach als policy.py. Eine 429 kann einen Header Retry-After tragen, und der Header kann Sekunden oder ein HTTP-Datum enthalten. MDN listet beide Formen auf und sagt, dass der Header bei einer 429 optional ist. Die Hilfsfunktion behandelt beide und fällt auf eine Zahl Ihrer Wahl zurück, wenn der Header fehlt oder nicht lesbar ist.
import re
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from urllib.parse import urljoin
from classify import classify
def retry_delay(resp, fallback):
raw = resp.headers.get("Retry-After")
if not raw:
return fallback
try:
return max(1.0, float(raw)) # delay-seconds
except ValueError:
pass
try:
when = parsedate_to_datetime(raw) # HTTP-date
return max(1.0, (when - datetime.now(timezone.utc)).total_seconds())
except (TypeError, ValueError):
return fallbackSchritt 5: Das Captcha lösen
Nur hier wird CapSkip aufgerufen. Die Funktion liest den sitekey und die Formular-Action aus der Seite, fragt CapSkip nach einem Token und sendet ihn im Feld g-recaptcha-response zurück. Sie behandelt absichtlich nur reCAPTCHA v2 und sendet nur dieses eine Feld und sonst nichts, sodass Sie bei einem Formular mit weiteren Pflichtfeldern diese den Daten hinzufügen müssen. Jedes Widget in der Liste bekommt das Label captcha, daher erreicht auch eine Seite mit Turnstile, hCaptcha, Friendly Captcha, CaptchaFox oder ALTCHA diese Funktion. Sie stoppt diese mit einem RuntimeError, der verlangt, zuerst den Captcha-Typ-Detektor auszuführen. Würden Sie stattdessen einen Turnstile-Schlüssel an die reCAPTCHA-Methode senden, endete das in ERROR_GOOGLEKEY: CapSkip lehnt einen Schlüssel ab, der nicht wie ein reCAPTCHA-Schlüssel aussieht, was ich an einem Mock bestätigt habe, der die Schlüsselprüfung der App nachbildet, nicht an der App. Zum reCAPTCHA-Aufruf selbst siehe die Seite zum reCAPTCHA-v2-Löser.
def solve_recaptcha_form(session, resp, solver):
"""reCAPTCHA v2 only. For other types, run the detector first."""
key = re.search(r"data-sitekey=[\"']([^\"']+)", resp.text)
form = re.search(r"<form[^>]*action=[\"']([^\"']*)", resp.text)
if "g-recaptcha" not in resp.text or not (key and form):
raise RuntimeError("not a plain reCAPTCHA v2 form: run the detector first")
token = solver.recaptcha(sitekey=key.group(1), url=resp.url)["code"]
return session.post(urljoin(resp.url, form.group(1)),
data={"g-recaptcha-response": token}, timeout=30)Schritt 6: die Abruf-Schleife
Die Schleife handelt nach der Entscheidung Captcha oder Sperre. Sie macht aus jedem Label eine Aktion und versucht es bis zu viermal. Ein Rate-Limit wartet so lange, wie der Header-Wert angibt, mit einer Untergrenze von 1 Sekunde und einer Obergrenze von 60. Durch die Obergrenze wird ein Retry-After von einem Tag nach einer Minute erneut versucht, was das Limit verlängern kann. Eine bloße 429, also eine ohne Retry-After, bekommt zwei Wartezeiten von je 5 Sekunden und dann einen Wechsel, und die Zählung beginnt für die neue Adresse von vorn. Bei vier Versuchen sind das zwei Wartezeiten, ein Wechsel und noch eine Wartezeit. Eine 429 mit Retry-After wechselt nie. Eine Sperre ruft Ihre Funktion rotate auf und erreicht den Solver nie. Ihre Funktion rotate sollte ändern, wie die nächste Anfrage aussieht, zum Beispiel durch Setzen von session.proxies, oder eine Exception auslösen, um den Lauf zu stoppen. Eine Challenge-Seite ruft Ihre Funktion on_challenge auf, die ein gültiges Cookie cf_clearance in session.cookies hinterlassen sollte, denn die Werte, die die Methode turnstile für eine Vollseiten-Challenge braucht, stehen nicht in dem HTML, das eine einfache Anfrage erhält. Nachdem eine der beiden Funktionen zurückgekehrt ist, fordert die Schleife die URL einfach erneut an. Bei Erfolg gibt die Funktion die Antwort auf den Formular-POST zurück; ist Ihre Zielseite eine andere, fordern Sie sie erneut an. Jeder Captcha-Versuch kostet einen Lösevorgang und kann bis zu fünf Minuten warten, sodass vier fehlschlagende Versuche vier Lösevorgänge verbrauchen können. Eine NetworkException bedeutet, dass CapSkip nicht erreichbar ist, und wird hier nicht abgefangen. Die cf_clearance-Anleitung behandelt das Cookie, das danach kommt.
def fetch(session, url, solver, rotate, on_challenge,
tries=4, sleep=time.sleep, expect=None):
limited = 0
for _ in range(tries):
resp = session.get(url, timeout=30)
kind = classify(resp, expect)
if kind == "ok":
return resp
if kind == "rate_limited":
limited += 1
if limited > 2 and "Retry-After" not in resp.headers:
rotate(session) # a bare 429 that outlasts two waits
limited = 0 # the new address starts fresh
else:
sleep(min(retry_delay(resp, 5.0), 60.0))
elif kind == "captcha":
resp = solve_recaptcha_form(session, resp, solver)
if classify(resp, expect) == "ok":
return resp
elif kind == "cloudflare_challenge":
on_challenge(session, resp) # yours: a browser, or the Turnstile guide
elif kind == "blocked":
rotate(session) # never solve a block
else:
raise RuntimeError(f"unknown response {resp.status_code} from {url}")
raise RuntimeError(f"gave up on {url} after {tries} tries")Ausführen
Speichern Sie dies als use.py neben den anderen beiden Dateien. Ersetzen Sie die Adresse und setzen Sie Ihre eigene Proxy-Logik in rotate ein. Das Beispiel-rotate löst eine Exception aus, sodass ein gesperrter Lauf stoppt, statt viermal dieselbe Adresse anzufragen. Führen Sie es mit dem Python der Umgebung aus, wie in .\.venv\Scripts\python.exe use.py. Der Port 8080 ist der Standard; verwenden Sie den Port aus den Verbindungseinstellungen von CapSkip, falls Ihrer abweicht. Mit der Platzhalter-Adresse endet das Skript mit einem Fehler zu einer unbekannten Antwort (unknown response), was das erwartete Ergebnis ist.
import requests
from capskip import CapSkip
from policy import fetch
solver = CapSkip(host="127.0.0.1", port=8080)
def rotate(session):
raise RuntimeError("blocked: change the proxy here, or stop the run")
def on_challenge(session, resp):
raise RuntimeError("Cloudflare challenge page: needs a browser")
session = requests.Session()
page = fetch(session, "https://example.com/data", solver, rotate, on_challenge)
print(page.status_code, len(page.text))Um zu sehen, was der Klassifikator aus echten Antworten macht, bevor Sie ihm vertrauen, führen Sie diese Prüfung auf ein paar eigenen URLs aus. In einem Lauf am 29. Oktober 2026 gab er ok für eine einfache Seite, rate_limited für eine 429 und blocked für eine 403 von einem Testdienst aus.
import requests
from classify import classify
URLS = [
"https://example.com/",
"https://httpbin.org/status/429",
"https://httpbin.org/status/403",
]
for url in URLS:
resp = requests.get(url, timeout=30)
print(classify(resp), resp.status_code, url)Wo der Klassifikator falsch liegt
Er ist eine Menge von Heuristiken, und diese Fälle sind bekannt.
| Fall | Ergebnis | Beheben |
|---|---|---|
| Inhaltsseite mit einem Captcha-Widget in der Fußzeile | captcha | Übergeben Sie expect an classify oder an fetch, mit einer Zeichenfolge, die nur die echte Seite enthält |
| Blogbeitrag mit unusual traffic im Titel | blocked | Ersetzen Sie die Formulierungen in BLOCK durch die Ihres Ziels |
| HTML-403 von der App selbst, etwa eine Anmeldeseite | blocked | Fügen Sie vor der 403-Regel eine Regel für diese Seite hinzu |
| Direkter Aufruf oder Weiterleitung auf eine Seite /captcha mit nur einem Bild | ok | Fügen Sie eine Regel für diese Website hinzu |
| Button mit der Klasse g-recaptcha und einem Attribut data-action (reCAPTCHA-v3-Bindung) | captcha | Dieses Widget hält die Seite nie zurück: Übergeben Sie expect oder führen Sie den Detektor aus |
| Captcha, das nach dem Laden per JavaScript hinzugefügt wird | ok | Der Klassifikator sieht nur die erste Antwort |
| reCAPTCHA-v3-Skript auf der ganzen Website | ok | Beabsichtigt: v3 hält die Seite nie zurück |
Häufige Fehler
| Fehler | Ursache | Beheben |
|---|---|---|
| RuntimeError: unknown response | Ein Status, den die Regeln nicht abdecken, etwa eine JSON-403 oder eine 404 | Geben Sie den Body aus und fügen Sie eine Regel hinzu, oder lassen Sie den Lauf anhalten |
| RuntimeError: gave up | Vier Versuche ohne ok | Prüfen Sie, welches Label sich wiederholt hat, und erhöhen Sie tries nur, wenn es rate_limited war |
| RuntimeError: not a plain reCAPTCHA v2 form | Das Label captcha kam von einem anderen Widget, oder die Seite hat keinen sitekey oder kein Formular | Führen Sie den Detektor aus und rufen Sie die passende Methode auf |
| ERROR_GOOGLEKEY | Der Schlüssel sieht nicht wie ein reCAPTCHA-Schlüssel aus, zum Beispiel ein Turnstile-Schlüssel oder das Wort explicit | Lesen Sie den Schlüssel aus dem Widget selbst oder führen Sie den Detektor aus |
| ApiException oder TimeoutException | Der Lösevorgang ist gescheitert oder hat das Limit des SDK überschritten | Prüfen Sie das Label davor und halten Sie tries niedrig, damit ein fehlschlagendes Captcha nicht viele Lösevorgänge verbraucht |
| NetworkException | CapSkip läuft nicht, oder Host oder Port sind falsch | Starten Sie CapSkip und prüfen Sie die Verbindungseinstellungen, siehe die API-Dokumentation |
Den Löser auf einem Server betreiben
Die Schleife braucht für CapSkip nur einen Host und einen Port. Im Local-Modus ist das 127.0.0.1 auf demselben PC. Im Server-Modus lauscht CapSkip auf Ihrer Netzwerkadresse oder einer öffentlichen IP, sodass ein Scraper auf einem anderen PC, einem VPS oder einer gehosteten Plattform dieselbe API aufruft. Ändern Sie Host und Port im Konstruktor von CapSkip, und nichts anderes ändert sich. Für einen stabilen Fernzugriff wird eine statische öffentliche IP empfohlen. Der Server-Modus läuft weiterhin auf Ihrer eigenen Hardware, und es wird weiterhin nichts pro Lösung abgerechnet. Er öffnet den Port für jede Adresse, fügen Sie daher in der Windows Firewall eine Blockierregel für die Adressen hinzu, die ihn nicht erreichen sollen. Die Verbindungseinstellungen erklären, wie Sie den Modus wechseln.
FAQ
Woran erkenne ich, ob ich gesperrt bin oder nur ein Rate-Limit erreicht habe?
Eine 429, besonders mit einem Header Retry-After, ist ein Rate-Limit, und Warten hilft. Eine 403 mit HTML-Body oder eine 200-Seite, die sagt, der Zugriff sei verweigert, ist eine Sperre. Eine bloße 429, die zwei Wartezeiten überdauert, wird als Faustregel als Sperre behandelt, weshalb die Schleife bei ihr den Proxy wechselt.
Kann ein gelöstes Captcha eine Sperre beheben?
Nein. Eine Sperre lehnt Ihre Adresse oder Ihren Fingerabdruck ab, und ein Token beweist über beides nichts. Die Schleife überspringt bei einer Sperre den Solver und wechselt stattdessen den Proxy.
Warum löst die Schleife die Cloudflare-Challenge-Seite nicht?
Die Methode turnstile braucht zwei Werte aus der Challenge, und eine einfache Anfrage erhält sie nicht. Die Schleife übergibt diesen Fall an eine Funktion, die Sie liefern, etwa eine, die einen echten Browser steuert, und hält den Klassifikator ehrlich in Bezug darauf, was er sehen kann.
Kann das gegen ein CapSkip auf einem anderen Rechner laufen?
Ja. Verwenden Sie den Server-Modus, richten Sie den Konstruktor von CapSkip auf diesen Host und Port, und die Schleife arbeitet wie auf einem einzigen PC. Der Solver bleibt auf Ihrer Hardware.
Die Kurzfassung
Erst entscheiden, ob Captcha oder Sperre, dann handeln: das Captcha lösen, das Rate-Limit abwarten, vor der Sperre zu einem anderen Proxy wechseln und bei allem Unbekannten anhalten. Mit CapSkip auf Ihrem eigenen Rechner ist die Ersparnis durch das Überspringen eines aussichtslosen Lösevorgangs Zeit, und die Ersparnis durch das Vermeiden unnötigen Wechselns sind Adressen. Für einen breiteren Überblick zum Umgang mit Captcha-Seiten: Die Captcha-Umgehung besteht darin, zu lösen, was sich lösen lässt, und um den Rest herumzuarbeiten. Zum Python-SDK selbst siehe die Python-Captcha-Solver-Seite.
