So lösen Sie Captchas in Crawl4AI, ohne die Sitzung zu verlieren

Crawl4AI hat keinen eigenen Captcha-Solver, ein Captcha-Ablauf in Crawl4AI besteht also aus drei Aufrufen, die Sie selbst verbinden: die Seite in einer benannten Sitzung laden, den sitekey mit einem externen Solver lösen und dann in denselben Tab zurückkehren, um den Token einzusetzen und abzusenden. Woran viele scheitern, ist die Reihenfolge, in der Crawl4AI die Dinge ausführt. Seit Version 0.8.5 läuft js_code nach wait_for, ein Crawl, der das Formular in js_code absendet und mit wait_for auf die nächste Seite wartet, bleibt also stehen, bis das Timeout greift. Das Absenden gehört in js_code_before_wait. Dieser Leitfaden geht den Sitzungsablauf mit reCAPTCHA v2 durch und zeigt dann einen Hook für Deep Crawls, bei denen Sie nicht jeden Aufruf selbst steuern.
Was Sie brauchen
- Python 3.10 oder neuer und Crawl4AI 0.9 oder neuer. Alles hier wurde aus dem Quellcode von 0.9.4 gelesen und gegen diese Version ausgeführt; die Reihenfolge in Schritt 3 gilt schon seit 0.8.5.
- Das CapSkip-Python-Paket, das Ihnen AsyncCapSkip liefert, einen wirklich asynchronen Client, der in den Event-Loop passt, auf dem Crawl4AI ohnehin läuft.
- Die URL der Seite mit dem Captcha und ein Selektor für etwas, das erst erscheint, wenn das Formular durchgegangen ist.
- CapSkip auf einem Windows-Rechner. Der Local-Modus antwortet auf 127.0.0.1, wenn der Crawler auf demselben Rechner läuft; der Server-Modus lauscht auf Ihrer Netzwerk- oder öffentlichen IP, wenn nicht. Beide finden Sie unter Verbindungseinstellungen.
# pip install crawl4ai pip install -U crawl4ai capskip # Downloads the browser Crawl4AI drives, once per machine. crawl4ai-setup
Schritt 1: Die Seite in einer Sitzung laden und den sitekey lesen
Geben Sie dem ersten Aufruf eine session_id. Damit bleibt der Tab offen, nachdem der Aufruf zurückgekehrt ist, mit intakten Cookies und intaktem Widget, sodass der Token, den Sie später lösen, in der Seite landet, die ihn angefordert hat. Ohne session_id schließt Crawl4AI die Seite, sobald es das HTML hat.
# pip install crawl4ai
import re
from crawl4ai import CrawlerRunConfig
PAGE_URL = "https://example.com/signup"
SESSION = "signup"
# The widget element, in any attribute order, with or without other classes.
WIDGET = r'<[^>]*class="(?:[^"]*\s)?g-recaptcha(?:\s[^"]*)?"[^>]*>'
async def read_sitekey(crawler):
# session_id keeps this tab open for the next arun call.
config = CrawlerRunConfig(session_id=SESSION)
first = await crawler.arun(PAGE_URL, config=config)
# Do not stop on first.success: a CAPTCHA page can be
# flagged as blocked while its HTML is complete.
widget = re.search(WIDGET, first.html)
match = widget and re.search(r'data-sitekey="([^"]+)"', widget.group(0))
return match.group(1) if match else NoneDieser Kommentar steht dort nicht ohne Grund. Crawl4AI 0.9 führt bei jedem Ergebnis eine Anti-Bot-Prüfung durch, und eine Seite, die es als Blockseite einstuft, wird als fehlgeschlagen markiert: success kommt als False zurück, und error_message beginnt mit Blocked by anti-bot protection. Jede HTML-Antwort mit 403 oder 503 zählt dazu, ebenso eine 429. Bei anderen Fehler-Statuscodes zählt eine Seite unter 10 KB dazu, wenn das class-Attribut ihres Widgets genau g-recaptcha lautet. Das gerenderte HTML steht trotzdem in first.html und enthält weiterhin den sitekey, den Sie brauchen, lesen Sie es also aus, bevor Sie entscheiden, dass der Crawl gescheitert ist.
Das Feld html ist die gerenderte Seite, ein per Skript erzeugtes Widget steht also ebenfalls darin. Wenn der Schlüssel nicht am Element steht, suchen Sie nach dem Parameter k= im src des Widget-iframes, und ergänzen Sie den ersten Aufruf um ein wait_for auf diesen iframe, falls das Widget spät rendert.
Schritt 2: Mit AsyncCapSkip lösen
Crawl4AI ist von oben bis unten asyncio, verwenden Sie also den asynchronen Client. Es handelt sich um eine echte Coroutine und nicht um einen Wrapper um blockierende Aufrufe, ein Lösen, das zwanzig Sekunden dauert, friert also nicht jeden anderen Crawl im selben Event-Loop ein.
# pip install capskip
from capskip import AsyncCapSkip
solver = AsyncCapSkip(host="127.0.0.1", port=8080)
async def solve(sitekey):
# Invisible v2 takes invisible=1, Enterprise takes enterprise=1.
result = await solver.recaptcha(sitekey=sitekey, url=PAGE_URL)
return result["code"] # the g-recaptcha-response valueIn Crawl4AI wartet währenddessen nichts, denn das Lösen findet zwischen zwei Aufrufen statt, nicht innerhalb eines Aufrufs. Der Tab bleibt einfach stehen. Eine Uhr läuft dagegen beim Token: Ein reCAPTCHA-Token ist nach seiner Ausstellung etwa zwei Minuten gültig, gehen Sie also direkt zum Absenden über. Einzelheiten finden Sie im Beitrag dazu, wie lange ein reCAPTCHA-Token gültig bleibt.
Schritt 3: Im selben Tab injizieren und absenden
Der zweite Aufruf verwendet die Sitzung wieder und setzt js_only. Damit weiß Crawl4AI, dass es JavaScript in der bereits geladenen Seite ausführen soll, statt die URL erneut zu laden. Ein Neuladen kostet einen zweiten Seitenaufruf, bei dem wieder von vorn eine Challenge kommen kann, und es setzt alles zurück, was der erste Ladevorgang eingerichtet hat, etwa ein teilweise ausgefülltes Formular.
import json
async def submit(crawler, token):
# Crawl4AI wraps this in an async function, so statements work.
inject = (
"document.getElementById('g-recaptcha-response').value = "
f"{json.dumps(token)};"
"document.querySelector('form').submit();"
)
config = CrawlerRunConfig(
session_id=SESSION,
js_only=True, # same tab, no reload
js_code_before_wait=inject, # runs BEFORE wait_for
wait_for="css:.signup-complete",
)
return await crawler.arun(PAGE_URL, config=config)Deshalb gehört das Skript in js_code_before_wait. Seit 0.8.5 und in jedem 0.9-Release führt ein Crawl zuerst js_code_before_wait aus, dann wait_for und zuletzt js_code, auf der fertigen Seite. Wenn Sie das Absenden also in js_code legen, beginnt wait_for nach der nächsten Seite zu suchen, bevor überhaupt etwas abgesendet wurde, und sucht weiter, bis page_timeout abläuft, standardmäßig nach 60 Sekunden. Dann scheitert der Aufruf mit Wait condition failed, und das Formular wird nie gesendet. Beispiele, die im selben Aufruf aus js_code absenden und mit wait_for warten, darunter eines in der eigenen Dokumentation von Crawl4AI, laufen unter 0.9.4 genau in dieses Problem.
Bauen Sie den String mit json.dumps, statt den Token zwischen Anführungszeichen einzufügen, denn ein JSON-String-Literal ist zugleich ein gültiges JavaScript-String-Literal, samt Escaping. Auf die Navigation müssen Sie nicht selbst warten: wait_for pollt über sie hinweg weiter und findet .signup-complete auf der nächsten Seite. Was Crawl4AI allerdings nicht tut, ist eine Exception zu werfen, wenn das Skript scheitert. Ein Syntaxfehler bekommt eine Log-Zeile, ein Laufzeitfehler dagegen, etwa eine Element-ID mit Tippfehler, wird ohne Log-Zeile verschluckt und zeigt sich nur daran, dass wait_for in ein Timeout läuft. Testen Sie die beiden Anweisungen also in der DevTools-Konsole auf der echten Seite, bevor Sie dem Solver die Schuld geben.
Manche Websites lesen die Textarea nie. Sie registrieren beim Widget einen Callback und senden von dort ab, ersetzen Sie die beiden Anweisungen also durch einen Aufruf der Funktion, die im Attribut data-callback genannt ist, und übergeben Sie ihr den Token. Darunter steckt weiterhin ganz gewöhnliches reCAPTCHA v2, erklärt auf der Seite zum reCAPTCHA-v2-Löser.
Schritt 4: In einem Deep Crawl mit einem Hook lösen
Der Sitzungsablauf löst ein Captcha in Crawl4AI, wenn Sie jeden arun-Aufruf selbst in der Hand haben. Ein Deep Crawl oder ein arun_many-Batch bietet Ihnen das nicht, hängen Sie das Lösen also stattdessen an den Hook after_goto. Er läuft auf der Playwright-Seite direkt nach der Navigation und vor wait_for, für jede URL, die der Crawler ansteuert (js_only-Aufrufe überspringen ihn), sodass Seiten ohne Widget einfach durchlaufen.
from capskip import CapSkipError
async def after_goto(page, context, url, response, **kwargs):
holder = await page.query_selector("div.g-recaptcha[data-sitekey]")
if holder is None:
return page # no widget, nothing to do
sitekey = await holder.get_attribute("data-sitekey")
try:
result = await solver.recaptcha(sitekey=sitekey, url=page.url)
except CapSkipError:
return page # keep the page HTML if the solve fails
async with page.expect_navigation():
await page.evaluate(
"t => { document.getElementById('g-recaptcha-response').value = t;"
" document.querySelector('form').submit(); }", result["code"])
return page
crawler.crawler_strategy.set_hook("after_goto", after_goto)Ein paar Dinge sollten Sie zu diesem Weg wissen. Der Hook läuft innerhalb des Crawls, die Lösezeit kommt also zu dieser Seite hinzu, und mehrere Seiten, die gleichzeitig auf ein Widget treffen, warten jeweils auf ihr eigenes Lösen, was AsyncCapSkip parallel abwickelt. Der Hook gilt global für den Crawler, halten Sie die Prüfung also schlank, denn jede Seite führt sie aus. Und der try-Block ist wichtig: Eine Exception, die aus dem Hook entkommt, lässt den gesamten Crawl dieser URL scheitern und liefert überhaupt kein HTML, ein Ausfall von CapSkip würde also sonst jede geschützte Seite in einem Deep Crawl leeren. Noch ein Haken: Crawl4AI behält den Statuscode der ersten Antwort. Eine Challenge, die als 403 ausgeliefert wird, kommt weiterhin mit success False und Blocked by anti-bot protection zurück, selbst wenn der Hook sie gelöst hat und result.html die Seite dahinter ist. Prüfen Sie bei diesen URLs das html auf Ihren Inhalt, statt sich auf success zu verlassen. Crawl4AI dokumentiert jeden Hook und seine Argumente auf seiner Hooks-Seite. Derselbe Hook eignet sich für jeden Crawler, der auf Playwright aufbaut, und das größere Bild finden Sie auf der Playwright-Captcha-Solver-Seite.
Wohin CapSkip gehört, wenn der Crawler woanders läuft
Crawl4AI landet oft auf einem Linux-Server oder in einem Container, sobald ein Crawl wächst, und CapSkip ist eine Windows-Anwendung, die beiden laufen also häufig auf verschiedenen Rechnern. Genau dafür gibt es den Server-Modus. Er lässt CapSkip auf Ihrer Netzwerkadresse oder öffentlichen IP statt auf 127.0.0.1 lauschen, sodass der Crawler es über dieselbe HTTP-API von überall aufrufen kann, von wo aus eine Route dorthin führt. Verwenden Sie eine statische öffentliche IP, wenn dieser Weg über das Internet führt, aktivieren Sie die Validierung des API-Schlüssels und beschränken Sie den Port mit einer Regel in der Windows Firewall auf die Adressen des Crawlers. Es bleibt ein Rechner, der Ihnen gehört, und abgerechnet wird pro Lösung darauf weiterhin nichts.
Das SDK liest Umgebungsvariablen nicht von selbst. Lesen Sie CAPSKIP_HOST, CAPSKIP_PORT und CAPSKIP_API_KEY in Ihrem eigenen Code aus und übergeben Sie die Werte, wie es das vollständige Beispiel tut.
Ein Detail von Crawl4AI, das Sie einplanen sollten: Seit 0.9.0 lehnt sein Docker-Server session_id, js_code und js_code_before_wait mit einem HTTP 400 ab, wenn sie über das Netzwerk ankommen, und er nimmt keinen Hook-Code mehr an. Keiner der beiden Wege in diesem Leitfaden funktioniert über die REST-API, führen Sie das Ganze also mit der Bibliothek in Ihrem eigenen Python-Prozess aus.
Vollständiges lauffähiges Beispiel
# pip install crawl4ai capskip
import asyncio
import json
import os
import re
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
from capskip import AsyncCapSkip
PAGE_URL = "https://example.com/signup"
SESSION = "signup"
WIDGET = r'<[^>]*class="(?:[^"]*\s)?g-recaptcha(?:\s[^"]*)?"[^>]*>'
solver = AsyncCapSkip(
apiKey=os.getenv("CAPSKIP_API_KEY", "capskip"),
host=os.getenv("CAPSKIP_HOST", "127.0.0.1"),
port=int(os.getenv("CAPSKIP_PORT", "8080")),
)
async def main():
async with AsyncWebCrawler() as crawler:
first = await crawler.arun(
PAGE_URL, config=CrawlerRunConfig(session_id=SESSION))
widget = re.search(WIDGET, first.html)
match = widget and re.search(r'data-sitekey="([^"]+)"', widget.group(0))
if not match:
raise RuntimeError(f"no sitekey found: {first.error_message}")
result = await solver.recaptcha(sitekey=match.group(1), url=PAGE_URL)
inject = (
"document.getElementById('g-recaptcha-response').value = "
f"{json.dumps(result['code'])};"
"document.querySelector('form').submit();"
)
done = await crawler.arun(PAGE_URL, config=CrawlerRunConfig(
session_id=SESSION,
js_only=True,
js_code_before_wait=inject,
wait_for="css:.signup-complete",
wait_for_timeout=30000,
))
print(done.success) # True once the next page has loaded
asyncio.run(main())wait_for_timeout gibt dem Warten nach dem Absenden ein eigenes Limit, sodass ein Absenden, das ins Leere läuft, nach 30 Sekunden scheitert statt nach den 60 von page_timeout. Ersetzen Sie .signup-complete durch etwas, das nur auf der Seite nach dem Formular existiert. Mehr zum Rest der Python-Landschaft, einschließlich Selenium und einfacher HTTP-Clients, finden Sie auf der Python-Captcha-Solver-Seite.
Häufige Fehler und was sie bedeuten
| Was Sie sehen | Ursache | Beheben |
|---|---|---|
| Wait condition failed nach etwa 60 Sekunden, und das Formular wurde nie abgesendet | Das Absenden steht in js_code, das seit 0.8.5 nach wait_for läuft | Verschieben Sie das Skript nach js_code_before_wait |
| first.success ist False mit Blocked by anti-bot protection | Die Blockprüfung von Crawl4AI hat die Captcha-Seite als Blockseite eingestuft | Lesen Sie den sitekey trotzdem aus first.html; die Seite ist intakt |
| wait_for läuft beim zweiten Aufruf in ein Timeout, und das html ist eine leere Seite | Die session_id weicht ab, daher hat Crawl4AI einen neuen, leeren Tab geöffnet | Verwenden Sie bei beiden Aufrufen dieselbe session_id |
| Der Token steht in der Textarea, aber die Website meldet, das Captcha sei fehlgeschlagen | Die Website sendet über eine data-callback-Funktion ab, oder der Token ist abgelaufen | Rufen Sie den Callback mit dem Token auf und senden Sie innerhalb von zwei Minuten nach dem Lösen ab |
| Überhaupt kein Fehler, dann läuft das Warten in ein Timeout | Das Injektionsskript hat innerhalb der Seite eine Exception geworfen, und Crawl4AI verschluckt das stillschweigend | Führen Sie das Skript zuerst in der DevTools-Konsole aus und prüfen Sie die IDs, die es verwendet |
| HTTP 400 vom Crawl4AI-Docker-Server | Der Server lehnt session_id und Skriptfelder ab, die über das Netzwerk ankommen | Führen Sie die Bibliothek in Ihrem eigenen Python-Prozess aus |
| NetworkException beim Lösen | CapSkip läuft nicht, oder Host und Port zeigen auf den falschen Rechner | Starten Sie CapSkip und verwenden Sie den Server-Modus, wenn der Crawler anderswo läuft |
| TimeoutException beim Lösen | Die Lösung hat recaptchaTimeout überdauert | Erhöhen Sie ihn im Konstruktor über den Standardwert von 300 Sekunden |
FAQ
Löst Crawl4AI Captchas selbst?
Nein. Es erkennt sie, in dem Sinne, dass seine Anti-Bot-Prüfung eine Captcha-Seite als blockierten Crawl markiert, und es kann eine blockierte Seite über eine Liste von Proxys erneut versuchen. Die Optionen magic und simulate_user ergänzen die Mausbewegungen und das Scrollen, auf die Anti-Bot-Systeme achten, was eine Challenge unwahrscheinlicher machen kann. Nichts davon löst ein Widget, das schon auf der Seite ist. Diese Lücke füllt ein externer Solver, und dieselbe Lücke beschreibt auch die Seite zum Captcha-Solver für Web Scraping.
Funktioniert derselbe Ablauf auch für Cloudflare Turnstile?
Für das Widget ja. Rufen Sie solver.turnstile mit dem sitekey und der Seiten-URL auf und schreiben Sie den Token in das versteckte Eingabefeld namens cf-turnstile-response statt in die reCAPTCHA-Textarea. Ganzseitige Challenges sind eine andere Aufgabe, denn ihr Token muss zusammen mit dem User-Agent reisen, den CapSkip zurückmeldet, der Browser, der ihn absendet, muss also genau diesen User-Agent vorweisen. Diesen Fall behandelt die Cloudflare-Turnstile-Löser-Seite.
Mein Crawler läuft in einem Container. Wohin kommt CapSkip?
Auf einen Windows-Rechner, den Sie kontrollieren, mit eingeschaltetem Server-Modus. Der Container erreicht CapSkip dann über die API wie jeden anderen internen Dienst, Crawler und Solver müssen sich also kein Betriebssystem teilen. Übergeben Sie die Adresse des Windows-Rechners als CAPSKIP_HOST, aktivieren Sie die Validierung des API-Schlüssels und geben Sie dem Crawler einen eigenen Schlüssel, damit er sich einzeln widerrufen lässt.
Wie lange bleibt eine Sitzung zwischen den beiden Aufrufen offen?
Weit länger, als ein Lösen dauert. Der Browser-Manager im Quellcode von 0.9.4 räumt Sitzungen auf, die 30 Minuten lang unbenutzt waren, und das Schließen des Crawlers schließt sie alle. Die Grenze, die in der Praxis zählt, ist die des Tokens, etwa zwei Minuten, Lösen und Absenden sollten also direkt aufeinander folgen.
Die Kurzfassung
Hier ist der gesamte Captcha-Ablauf in Crawl4AI in einem Absatz. Laden Sie die Seite mit einer session_id und lesen Sie den sitekey aus dem HTML, auch wenn Crawl4AI den Crawl als blockiert meldet. Lösen Sie ihn mit AsyncCapSkip. Kehren Sie mit js_only=True in denselben Tab zurück, setzen Sie den Token ein und senden Sie aus js_code_before_wait ab, und warten Sie mit wait_for und einem eigenen Timeout auf die nächste Seite. Bei Deep Crawls tun Sie dasselbe aus dem Hook after_goto heraus. Richten Sie den Client auf eine Server-Modus-Adresse, wenn der Crawler auf einem anderen Rechner läuft.
Noch eine letzte Sache, die verändert, wie Sie einen Crawl dimensionieren. Da die Captcha-Umgehung auf einem Rechner läuft, der Ihnen bereits gehört, kostet ein Crawl, der auf jeder Seite auf ein Widget trifft, genauso viel wie einer, der nur einmal darauf trifft. Es gibt also keinen Grund, eine URL nur deshalb auszulassen, weil sie hinter einer Challenge liegt.
