Captcha in DrissionPage lösen und das Token injizieren

Ein Captcha-Schritt in DrissionPage besteht aus drei Zügen: den sitekey von der Seite lesen, ihn an einen Löser schicken und das Token dann per JavaScript zurück in das Formular schreiben. DrissionPage steuert ein echtes Chrome über das DevTools Protocol statt über einen Webdriver, die Browser-Seite ist also einfacher als bei Selenium. Das Elementmodell ist es nicht, und es bringt einen Standardwert mit, der aus einem fehlenden Widget einen Fehler des Lösers statt eines Fehlers Ihres Locators macht. Dieser Standardwert ist der Grund, warum die meisten dieser Skripte auf verwirrende Weise scheitern, deshalb bekommt er einen eigenen Abschnitt.
Was Sie brauchen
- Python 3.10 oder neuer, mit installiertem DrissionPage 4.1 und dem CapSkip SDK.
- Ein Chromium-Browser, den DrissionPage starten kann oder an den es sich anhängen kann.
- 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 das Skript auf einem anderen Rechner läuft. Beides ist beschrieben unter Verbindungseinstellungen.
# Both packages, one line. pip install DrissionPage capskip
Warum DrissionPage die Form der Sache verändert
DrissionPage spricht direkt über CDP mit Chrome. Es gibt keinen chromedriver-Prozess dazwischen, also auch keine Treiber-Binary zum Patchen und keinen separaten Dienst, den Sie mit Ihrer Browserversion im Gleichschritt halten müssen. Außerdem hängt es sich an einen bereits laufenden Browser an, was hier wirklich nützlich ist: Sie melden sich einmal von Hand an, lassen das Profil offen und lassen das Skript die Sitzung übernehmen.
Was sich nicht ändert, ist das Token. Ein gelöstes reCAPTCHA ist ein String, der vor dem Absenden im versteckten g-recaptcha-response Textarea landen muss. Dieses Textarea ist display:none, weshalb Tippen dort in keinem Automatisierungswerkzeug eine Option ist. Sie schreiben es mit JavaScript, und bei DrissionPage übernimmt das die Methode run_js.
Eines sollte man klar sagen: kein Webdriver bedeutet nicht keine Erkennung. Chrome über CDP zu fahren entfernt ein Signal und lässt den Rest Ihres Fingerprints genau dort, wo er war. Die Challenge zu lösen ist eine andere Aufgabe, als wie ein Browser auszusehen, und dieser Beitrag behandelt nur die erste.
Schritt 1: den Sitekey von der Seite lesen
Der sitekey steht im Host-Dokument, nicht im iframe des Widgets. Das Markup von Google selbst platziert ihn auf einem Container-Element als data-sitekey, und der Attribut-Locator von DrissionPage findet ihn ohne CSS-Selektor.
# pip install DrissionPage
from DrissionPage import ChromiumPage
page = ChromiumPage()
page.get("https://example.com/page-with-recaptcha")
# @attr finds by attribute. The default search timeout is 10s,
# which is plenty for a widget that renders on load.
holder = page.ele("@data-sitekey")
sitekey = holder.attr("data-sitekey")
print(sitekey) # 6LxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxDie Locator-Präfixe von DrissionPage lohnen sich hier, denn sie ersetzen das meiste XPath, das Sie sonst schreiben würden: @attr=value trifft exakt, @attr:value trifft einen Teilstring, @attr^value trifft den Anfang und @attr$value trifft das Ende. Stellen Sie einem Locator einen Tag-Namen voran, wie bei tag:iframe, legen Sie zusätzlich den Elementtyp fest.
Manche Seiten setzen den sitekey nie auf die Host-Seite und übergeben ihn nur in der iframe-URL. Lesen Sie ihn dann von dort und nutzen Sie contains, um den gewünschten Frame zu treffen.
# Fallback: the k= query parameter on the widget iframe.
from urllib.parse import urlparse, parse_qs
frame = page.ele("tag:iframe@src:recaptcha/api2/anchor")
src = frame.attr("src")
sitekey = parse_qs(urlparse(src).query)["k"][0]Schritt 2: gegen die lokale API lösen
Der Löser läuft auf Ihrer eigenen Maschine auf Port 8080 und spricht die 2captcha-API, der SDK-Aufruf ist also eine Zeile und dahinter steckt keine Abrechnung pro Lösung. Übergeben Sie die Seiten-URL aus page.url statt sie erneut einzutippen, denn eine Weiterleitung zwischen get() und dem Lösen würde Sie sonst die falsche URL absenden lassen.
# pip install capskip from capskip import CapSkip solver = CapSkip(host="127.0.0.1", port=8080) # Same call shape for v3 (version="v3") and Enterprise (enterprise=1). result = solver.recaptcha(sitekey=sitekey, url=page.url) token = result["code"] # the g-recaptcha-response value
Turnstile und GeeTest haben eigene Methoden, solver.turnstile() und solver.geetest(), und beide haben dieselbe Form wie der Aufruf oben. Die vollständige Parameterliste für beide finden Sie hier: der CapSkip-API-Dokumentation.
Schritt 3: den Token einfügen und absenden
DrissionPage übergibt zusätzliche Argumente an run_js positionell, und das Skript liest sie als arguments[0] und so weiter. Übergeben Sie das Token als Argument, statt das JavaScript mit einem f-string zusammenzubauen: Ein Token ist lang, undurchsichtig und gelegentlich voller Zeichen, über deren Quoting Sie lieber nicht nachdenken möchten.
# Extra args arrive as arguments[0], arguments[1], ...
page.run_js(
"document.getElementById('g-recaptcha-response').value = arguments[0];",
token,
)
# Then submit the form the way the page expects.
page.ele("tag:button@type=submit").click()Wenn die Seite einen Callback definiert, statt beim Absenden das Textarea zu lesen, rufen Sie ihn nach dem Setzen des Werts auf. Das ist ein seitenspezifischer Funktionsname, lesen Sie ihn also aus dem Markup der Seite selbst aus, statt zu raten, und beachten Sie: Es bleibt ganz normales reCAPTCHA v2, der Callback ändert nur, wie Sie das Token übergeben, nicht, wie es gelöst wird. Die Seite zum reCAPTCHA-v2-Löser behandelt beide Übergabewege.
Der stille Fehlschlag, der die meiste Zeit kostet
DrissionPage wird mit Settings.raise_when_ele_not_found auf False ausgeliefert. Ein Locator, der nichts trifft, wirft keine Exception. Er liefert ein NoneElement zurück, das falsy ist und sich mit dem None von Python als gleich vergleicht, und Ihr Skript läuft weiter.
Das ist eine bewusste Design-Entscheidung, und sie ist angenehm, solange Sie optionale Elemente abtasten. Hier ist sie unangenehm, denn als Nächstes lesen Sie ein Attribut aus, und der Fehler, den Sie bekommen, nennt den Locator statt das Widget. Schlimmer ist der Fall, in dem das Element tatsächlich existiert, aber gar kein data-sitekey-Attribut trägt. Dann gibt attr() None zurück, dieses None wandert als leerer Key bis zum Löser durch, und der Fehlschlag zeigt sich am Ende als ERROR_GOOGLEKEY aus einem API-Aufruf, den Sie gar nicht verdächtigt hatten.
Zwei Zeilen beheben das. Schalten Sie das Werfen von Exceptions ein und prüfen Sie den sitekey, bevor Sie eine Lösung dafür verbrauchen.
# Make a missing element fail where it happened.
from DrissionPage.common import Settings
Settings.set_raise_when_ele_not_found(True)
# And still check the value, because a found element can hold nothing.
if not sitekey:
raise RuntimeError("No sitekey on the page. Check the widget rendered.")Vollständiges lauffähiges Beispiel
Alles von oben in einem Skript. Der finally-Block ist wichtiger als sonst, denn DrissionPage kann sich an einen Browser anhängen, den Sie nicht gestartet haben, und einen geerbten Browser zu beenden ist selten das, was Sie wollen.
# pip install DrissionPage capskip
from DrissionPage import ChromiumPage
from DrissionPage.common import Settings
from capskip import CapSkip
Settings.set_raise_when_ele_not_found(True)
PAGE_URL = "https://example.com/page-with-recaptcha"
page = ChromiumPage()
solver = CapSkip(host="127.0.0.1", port=8080)
try:
page.get(PAGE_URL)
sitekey = page.ele("@data-sitekey").attr("data-sitekey")
if not sitekey:
raise RuntimeError("Widget found but data-sitekey was empty.")
result = solver.recaptcha(sitekey=sitekey, url=page.url)
page.run_js(
"document.getElementById('g-recaptcha-response').value = arguments[0];",
result["code"],
)
page.ele("tag:button@type=submit").click()
page.wait.doc_loaded()
print(page.title) # the page you land on after submitting
finally:
page.quit()Den Löser auf einer anderen Maschine betreiben
Scraper wandern früher oder später auf einen Server, und DrissionPage wandert mit: Es braucht eine Chromium-Binary und einen Display-Puffer, viel mehr nicht. Der Löser muss ihm nicht auf dieselbe Maschine folgen.
CapSkip hat zwei Verbindungsmodi. Lokal bindet an 127.0.0.1 und ist nur von diesem Gerät aus erreichbar, was beim Schreiben des Skripts die richtige Einstellung ist. Server bindet an Ihre Netzwerkadresse oder öffentliche IP, sodass eine Scraping-VM, ein Container-Host oder eine zweite Workstation denselben Löser über die API aufrufen kann. Eine statische öffentliche IP hält diese Adresse stabil. Am Code ändert sich nichts außer der Host-Adresse, die Sie übergeben, und an den Kosten ändert sich ebenfalls nichts, denn es bleibt Ihre Hardware.
# Same SDK, same call. Only the host moves. solver = CapSkip(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.
Häufige Fehler und was sie bedeuten
| Was Sie sehen | Ursache | Beheben |
|---|---|---|
| ElementNotFoundError bei attr() | Der Locator hat nichts getroffen und ein NoneElement geliefert | Den Locator weiter fassen oder warten, bis das Widget gerendert ist |
| Das Skript läuft an einem fehlenden Element vorbei | raise_when_ele_not_found steht standardmäßig auf False | Settings.set_raise_when_ele_not_found(True) |
| ERROR_GOOGLEKEY | attr() lieferte None, und ein leerer Key wurde abgeschickt | Den Wert prüfen oder den Parameter k aus dem iframe lesen |
| 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 |
| Das Formular weist einen Token ab, der sauber gelöst wurde | Das Token war vor dem Absenden abgelaufen | Unmittelbar vor dem Absenden lösen, nicht beim Laden der Seite |
FAQ
Sollte ich ChromiumPage oder die neuere Chromium-Klasse nutzen?
Beides geht. Version 4.1 exportiert Chromium, ChromiumPage, SessionPage und WebPage aus dem Top-Level-Paket, und ChromiumPage ist immer noch der kürzeste Weg zu einem Tab. Die Captcha-Arbeit ist identisch, wofür Sie sich auch entscheiden, denn das Lesen des sitekeys, das Lösen und das Injizieren des Tokens laufen alle über das Tab-Objekt.
Muss ich in das reCAPTCHA-iframe wechseln?
Nein, und genau hier überkonstruieren viele. Die Checkbox lebt in einem iframe, aber das sitekey-Attribut und das versteckte Response-Textarea gehören beide zum Host-Dokument. Einen Frame fassen Sie nur an, wenn die Seite den sitekey nicht herausrückt und Sie ihn aus der URL des Frames selbst lesen müssen.
Warum übergibt mein Skript kommentarlos einen sitekey mit dem Wert None?
Weil DrissionPage standardmäßig einen falsy Platzhalter zurückgibt, statt eine Exception zu werfen, und weil einem existierenden Element trotzdem das Attribut fehlen kann, nach dem Sie gefragt haben. Beide Wege enden mit einem None in einer Variablen, die Sie für einen String gehalten haben. Schalten Sie oben in der Datei das Werfen von Exceptions ein und ergänzen Sie eine explizite Prüfung des Werts, dann sind beide Fälle abgedeckt.
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.
Die Kurzfassung
Lesen Sie den sitekey mit einem Attribut-Locator, lösen Sie ihn auf 127.0.0.1:8080, injizieren Sie das Token mit run_js und positionellen Argumenten, dann senden Sie ab. Schalten Sie zuerst raise_when_ele_not_found ein, bevor Sie irgendetwas davon tun, denn der Standardwert verbirgt genau den Fehlschlag, auf den Sie zuerst stoßen. Das größere Python-Bild samt Selenium und Playwright finden Sie hier: der Python-Captcha-Solver-Seite. Und weil der Löser ein lokaler Captcha-Löser und kein verbrauchsabhängiger Dienst ist, kostet ein Crawl, der tausend Seiten erneut versucht, genauso viel wie einer, der zehn erneut versucht.
