So lösen Sie Captchas in nodriver mit dem Async-SDK

Ein Captcha-Schritt in nodriver besteht aus den üblichen drei Zügen: den sitekey von der Seite lesen, ihn an einen Löser schicken, das Token per JavaScript zurückschreiben. Neu ist, dass nodriver von Grund auf asynchron ist. Es steuert Chrome über einen asyncio-Websocket, eine blockierende Lösung lässt Ihr Skript also nicht nur warten, sie legt den Socket lahm, der jede DevTools-Nachricht transportiert. Kombinieren Sie es mit dem asynchronen Client, und das Ganze bleibt reaktionsfähig.
Was Sie brauchen
- Python 3.10 oder neuer, mit installiertem nodriver 0.50 und dem CapSkip SDK.
- Chrome, Chromium, Edge oder Brave, installiert dort, wo das Skript läuft. nodriver startet den Browser direkt.
- Die Seiten-URL des geschützten Formulars. Den sitekey lesen Sie zur Laufzeit aus.
- CapSkip im Lokal-Modus, wenn Skript und Löser auf derselben Maschine liegen, oder im Server-Modus, wenn nicht. Beides ist beschrieben unter Verbindungseinstellungen.
# Both packages, one line. pip install nodriver capskip
Warum nodriver die Form der Sache verändert
nodriver ist der offizielle Nachfolger von undetected-chromedriver, geschrieben vom selben Autor, und seine Schlagzeile lautet: kein Webdriver und kein Selenium irgendwo im Stack. Es spricht das DevTools Protocol mit einem Browser, den es selbst gestartet hat. Keine chromedriver-Binary zum Patchen, keine Treiberversion, die Sie mit Chrome im Gleichschritt halten müssen.
Für die Captcha-Arbeit zählt die zweite Hälfte dieses Satzes: Es ist vollständig asynchron. Die Verbindung ist ein Websocket, den asyncio verwaltet, und eine Hintergrundaufgabe liest Protokollnachrichten davon ab. Jede Elementsuche, jede Navigation und jeder Event-Handler hängt davon ab, dass diese Aufgabe eingeplant wird. Rufen Sie mittendrin einen synchronen Löser auf, läuft für die Dauer der Lösung nichts anderes im Prozess, und das sind bei reCAPTCHA v2 routinemäßig fünfzehn bis fünfundvierzig Sekunden.
Die Regel für dieses Framework ist deshalb kurz. Nutzen Sie den asynchronen Client und rufen Sie ihn mit await auf.
Eines sollte man klar sagen: kein Webdriver bedeutet nicht keine Erkennung. Den Treiber zu entfernen entfernt ein Signal und lässt den Rest Ihres Fingerprints dort, wo er war. Eine Challenge zu lösen und wie ein Browser auszusehen sind zwei verschiedene Aufgaben, und dieser Beitrag behandelt die erste.
Schritt 1: den Sitekey von der Seite lesen
Der sitekey liegt im Host-Dokument, nicht im iframe des Widgets. Googles Markup setzt ihn als data-sitekey-Attribut auf einen Container, und die select-Methode von nodriver findet diesen Container per CSS-Selektor. Achten Sie in der letzten Zeile auf den Klammerzugriff, denn genau der bringt die Leute zu Fall.
# pip install nodriver
import nodriver as uc
async def main():
browser = await uc.start()
page = await browser.get("https://example.com/page-with-recaptcha")
# select() retries for 10 seconds by default, so it doubles
# as a wait condition for a widget that renders late.
holder = await page.select("div.g-recaptcha")
sitekey = holder.attrs["data-sitekey"]
print(sitekey) # 6Lxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
uc.loop().run_until_complete(main())Attributnamen liegen am Element genau so, wie das HTML sie geschrieben hat, samt Bindestrichen, der Klammerzugriff ist deshalb der einzig verlässliche Weg, eines davon zu lesen. Die Punkt-Abkürzung sieht aus, als müsste sie funktionieren, und tut es stillschweigend nicht: Fragen Sie ein Element nach data_sitekey, bekommen Sie None zurück, statt dass eine Exception fliegt, denn diese Suche fällt auf einen Standardwert durch. Dieses None wandert dann als leerer Key zum Löser und zeigt sich viel später als ERROR_GOOGLEKEY, weit weg von der Zeile, die es verursacht hat. Es gibt genau eine echte Umbenennung, die Sie kennen sollten: Das Attribut class liegt unter class_, damit es dem Python-Schlüsselwort nicht in die Quere kommt.
Manche Seiten geben den sitekey nie auf der Host-Seite preis und übergeben ihn nur in der iframe-URL des Widgets. Lesen Sie ihn dann aus dem Query-String.
# Fallback: the k= parameter on the anchor iframe.
from urllib.parse import urlparse, parse_qs
frame = await page.select("iframe[src*='recaptcha/api2/anchor']")
sitekey = parse_qs(urlparse(frame.attrs["src"]).query)["k"][0]Schritt 2: lösen, ohne den Socket zu blockieren
Das Python SDK bringt zwei Clients mit. CapSkip ist synchron, und AsyncCapSkip ist eine echte asyncio-Implementierung statt eines Alias, also genau das, was dieses Framework braucht. Beide sprechen mit dem Löser auf Ihrer eigenen Maschine auf Port 8080, und keiner von beiden rechnet pro Lösung ab.
# pip install capskip from capskip import AsyncCapSkip solver = AsyncCapSkip(host="127.0.0.1", port=8080) # Same call shape for v3 (version="v3") and Enterprise # (enterprise=1). Invisible v2 takes invisible=1. result = await solver.recaptcha(sitekey=sitekey, url=PAGE_URL) token = result["code"] # the g-recaptcha-response value
Weil der Aufruf awaitable ist, können mehrere Tabs gleichzeitig lösen, ganz ohne Threading. Öffnen Sie die Seiten, sammeln Sie die Lösungen ein und injizieren Sie dann jedes Token in den Tab, zu dem es gehört. Das größere Muster, samt der Frage, wie das SDK sein Polling abbremst, statt in festen Intervallen zu schlafen, ist beschrieben unter Captchas parallel lösen.
# Three tabs, three solves, one wait.
import asyncio
results = await asyncio.gather(*[
solver.recaptcha(sitekey=k, url=u) for k, u in targets
])Turnstile und GeeTest haben eigene Methoden, und beide haben dieselbe Form wie der Aufruf oben. Die vollständigen Parameterlisten für beide stehen in der CapSkip-API-Dokumentation.
Schritt 3: den Token einfügen und absenden
Das Antwort-Textarea ist mit display:none versteckt, Hineintippen ist also in keinem Automatisierungswerkzeug eine Option. Sie schreiben es per JavaScript. Die evaluate-Methode von nodriver nimmt einen Ausdruck als String entgegen, und daneben lassen sich keine Argumente übergeben, das Token muss also in diesen String eingebettet werden, und der sichere Weg dafür ist json.dumps statt eines f-strings. Ein JSON-String-Literal ist ein gültiges JavaScript-String-Literal, samt Quoting und Escaping.
# json.dumps gives a correctly quoted JS string literal.
import json
await page.evaluate(
"document.getElementById('g-recaptcha-response').value = "
+ json.dumps(token)
)
# Then submit the form the way the page expects.
button = await page.select("button[type=submit]")
await button.click()Zu prüfen, ob der Wert angekommen ist, hat eine eigene Falle, und die sind zehn Sekunden Ihrer Aufmerksamkeit wert. Mit gesetztem return_by_value gibt evaluate den einfachen Python-Wert nur dann zurück, wenn dieser Wert truthy ist. Ein leerer String oder eine Null fällt durch, und Sie bekommen stattdessen ein Protokollobjekt. Lesen Sie deshalb nicht die Länge aus und prüfen sie, denn eine Länge von null ist genau der Fall, den Sie erkennen wollen. Geben Sie etwas zurück, das niemals falsy sein kann.
# String() keeps a zero-length answer truthy, so the check
# reports the real number instead of a protocol object.
length = await page.evaluate(
"String(document.getElementById('g-recaptcha-response').value.length)",
return_by_value=True,
)
print(length) # "0" means the injection did not landDefiniert die Seite einen Callback, statt das Textarea beim Absenden auszulesen, rufen Sie ihn auf, nachdem Sie den Wert gesetzt haben. Der Funktionsname ist seitenspezifisch, lesen Sie ihn also aus dem Markup der Seite selbst aus, statt zu raten. Es bleibt so oder so gewöhnliches reCAPTCHA v2: Der Callback ändert, wie Sie das Token übergeben, nicht, wie es gelöst wird. Die Seite zum reCAPTCHA-v2-Löser behandelt beide Übergabewege.
Vollständiges lauffähiges Beispiel
Alles von oben in einem Skript. Der Löser wird einmal erzeugt und wiederverwendet, und der Browser wird in einem finally-Block gestoppt, damit eine fehlgeschlagene Lösung keinen Chrome-Prozess zurücklässt.
# pip install nodriver capskip
import json
import nodriver as uc
from capskip import AsyncCapSkip
PAGE_URL = "https://example.com/page-with-recaptcha"
async def main():
solver = AsyncCapSkip(host="127.0.0.1", port=8080)
browser = await uc.start()
try:
page = await browser.get(PAGE_URL)
holder = await page.select("div.g-recaptcha")
sitekey = holder.attrs["data-sitekey"]
if not sitekey:
raise RuntimeError("Widget found but data-sitekey was empty.")
result = await solver.recaptcha(sitekey=sitekey, url=PAGE_URL)
await page.evaluate(
"document.getElementById('g-recaptcha-response').value = "
+ json.dumps(result["code"])
)
button = await page.select("button[type=submit]")
await button.click()
await page.sleep(2)
print(page.target.url) # the page you land on after submitting
finally:
browser.stop()
uc.loop().run_until_complete(main())Den Löser auf einer anderen Maschine betreiben
nodriver landet früher oder später auf einem Server, und dort hat es zwei Bedürfnisse: eine Chromium-Binary und eine Desktop-Sitzung, auf der Chrome zeichnen kann. Der Headless-Modus ist standardmäßig aus, ein Server ohne eine solche Sitzung braucht Headless also ausdrücklich eingeschaltet. Der Löser muss diesen Umzug nicht mitmachen.
CapSkip hat zwei Verbindungsmodi. Der Lokal-Modus bindet an 127.0.0.1 und antwortet nur diesem Gerät, was beim Schreiben des Skripts die richtige Einstellung ist. Der Server-Modus bindet an Ihre Netzwerkadresse oder öffentliche IP, sodass eine Scraping-VM, ein Container-Host oder eine zweite Workstation denselben Löser über die API aufruft. Eine statische öffentliche IP hält diese Adresse stabil. Am Code ändert sich nichts außer dem Host, den Sie übergeben, und an den Kosten ändert sich ebenfalls nichts, denn es bleibt Ihre Hardware.
# Same SDK, same call. Only the host moves. solver = AsyncCapSkip(host="10.0.0.12", port=8080, apiKey="YOUR_API_KEY")
Schalten Sie die Schlüsselvalidierung ein, sobald der Löser auf einer Netzwerkadresse lauscht, und geben Sie jeder Maschine einen eigenen Schlüssel, damit einer widerrufen werden kann, ohne die anderen anzufassen. Die Einrichtungsanleitung führt durch beide Modi.
Eine Namenskollision sollten Sie dabei auseinanderhalten. Auch die start-Funktion von nodriver nimmt einen host und einen port entgegen, und die beschreiben einen Chrome-Debugging-Endpunkt, an den Sie sich anhängen wollen, nicht den Löser. Geben Sie beide an, startet nodriver überhaupt keinen Browser. Die Adresse des Lösers gehört in den Konstruktor des Clients und sonst nirgendwohin.
Häufige Fehler und was sie bedeuten
| Was Sie sehen | Ursache | Beheben |
|---|---|---|
| AttributeError bei einer attrs-Abfrage | select() hat nichts gefunden und None zurückgegeben | Den Selektor weiter fassen oder das Timeout von select erhöhen |
| Der sitekey ist None, ganz ohne Fehler | Der Punktzugriff erreicht ein Attribut mit Bindestrich nicht | Ihn mit Klammerzugriff aus attrs lesen |
| ERROR_GOOGLEKEY | Ein leerer sitekey ist beim Löser angekommen | Den Wert prüfen, bevor Sie eine Lösung dafür verbrauchen |
| Das Skript hängt während der gesamten Lösung | Ein synchroner Client hat den Event-Loop blockiert | AsyncCapSkip nutzen und den Aufruf mit await ausführen |
| NetworkException | CapSkip läuft nicht, oder der Host ist falsch | Die App starten oder host auf die Serveradresse zeigen lassen |
| TimeoutException | Die Lösung hat recaptchaTimeout überdauert | Über den Standardwert von 300 Sekunden anheben |
| evaluate liefert ein Objekt statt eines Strings | Der Wert war falsy, deshalb blieb die einfache Rückgabe aus | Die Länge in String() verpacken, nicht den Wert selbst |
FAQ
Kann ich den synchronen Client behalten, wenn ich nur einmal löse?
Können Sie, und bei einem kurzen Skript merken Sie es vielleicht nie. Was Sie eintauschen, ist jede Protokollnachricht, die während der Lösung eintrifft: Navigationsereignisse, Load-Events, alles, worauf ein Event-Handler gewartet hat. Bei einem langen Lauf zeigt sich das als Suchvorgänge, die ohne sichtbaren Grund ins Timeout laufen. Der asynchrone Client kostet einen Import und ein await, es gibt also wenig Grund, diesen Tausch einzugehen.
Muss ich in das reCAPTCHA-iframe wechseln?
Nein, und genau hier bauen die Leute zu viel. Die Checkbox liegt in einem iframe, aber das sitekey-Attribut und das versteckte Antwort-Textarea gehören beide zum Host-Dokument. Einen Frame fassen Sie nur an, wenn die Seite den sitekey zurückhält und Sie ihn aus der URL des Frames selbst lesen müssen.
Ich steige von undetected-chromedriver um. Was lässt sich übernehmen?
Die drei Züge lassen sich unverändert übernehmen, denn sie waren nie treiberspezifisch: den sitekey lesen, ihn lösen, das Token in das Textarea schreiben. Nicht übernehmen lässt sich die API drumherum, denn jeder Aufruf ist jetzt awaitable und ein Treiberobjekt gibt es nicht mehr. nodriver bringt außerdem einen Helfer mit, der eine laufende undetected-chromedriver-Instanz in ein Browser-Objekt umwandelt, womit Sie ein Skript schrittweise umziehen können. Den älteren Ansatz beschreibt die undetected-chromedriver-Captcha-Anleitung.
Mein Scraper läuft auf einem VPS. Wohin gehört der Löser?
Wohin Sie möchten, solange sich beide erreichen können. Der Server-Modus lässt den Löser auf einer Netzwerkadresse statt auf der Loopback-Adresse lauschen, der VPS ruft ihn also über die API auf wie jeden internen Dienst. Zeigen Sie mit dem host-Argument auf diese Adresse, aktivieren Sie die Schlüsselvalidierung und geben Sie dem VPS einen eigenen Schlüssel. Der Löser braucht kein Display, was praktisch ist, weil der Browser eines braucht.
Die Kurzfassung
Lesen Sie den sitekey mit Klammerzugriff auf attrs, lösen Sie ihn mit AsyncCapSkip auf 127.0.0.1:8080, injizieren Sie das Token über evaluate mit json.dumps und senden Sie dann ab. Nutzen Sie überall await, denn eine synchrone Lösung blockiert den Socket, der den Browser steuert. Das größere Python-Bild samt Selenium und Playwright finden Sie auf der Python-Captcha-Solver-Seite.
Eine Konsequenz sollte klar benannt werden, bevor Sie einen Crawl hochskalieren. Weil CapSkip ein unbegrenzter Captcha-Löser ist, der auf Ihrer eigenen Hardware läuft, kostet ein Lauf, der tausend Seiten erneut versucht, genauso viel wie einer, der zehn erneut versucht.
