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

nodriver captcha - How to Solve CAPTCHA in nodriver With the 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 land

Definiert 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 sehenUrsacheBeheben
AttributeError bei einer attrs-Abfrageselect() hat nichts gefunden und None zurückgegebenDen Selektor weiter fassen oder das Timeout von select erhöhen
Der sitekey ist None, ganz ohne FehlerDer Punktzugriff erreicht ein Attribut mit Bindestrich nichtIhn mit Klammerzugriff aus attrs lesen
ERROR_GOOGLEKEYEin leerer sitekey ist beim Löser angekommenDen Wert prüfen, bevor Sie eine Lösung dafür verbrauchen
Das Skript hängt während der gesamten LösungEin synchroner Client hat den Event-Loop blockiertAsyncCapSkip nutzen und den Aufruf mit await ausführen
NetworkExceptionCapSkip läuft nicht, oder der Host ist falschDie App starten oder host auf die Serveradresse zeigen lassen
TimeoutExceptionDie Lösung hat recaptchaTimeout überdauertÜber den Standardwert von 300 Sekunden anheben
evaluate liefert ein Objekt statt eines StringsDer Wert war falsy, deshalb blieb die einfache Rückgabe ausDie 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.