Captchas auf Selenium Grid lösen (RemoteWebDriver)

Eine Captcha-Lösung auf Selenium Grid funktioniert genau wie eine lokale, mit einem einzigen Unterschied, über den die meisten stolpern. Der Browser läuft auf dem Node. Ihr Testcode nicht. Der CapSkip-Aufruf passiert in Ihrem Testprozess, der Löser muss also von dort erreichbar sein, wo Sie pytest ausführen, und nicht von der Maschine, die den Browser hostet. Wenn Sie diese eine Tatsache richtig herum haben, ist der Rest derselbe Code, den Sie auch gegen ein lokales Chrome schreiben würden.
Was Sie brauchen
- Ein Selenium Grid, das Sie erreichen können. Standalone, Hub und Node oder vollständig verteilt verhalten sich von der Client-Seite aus alle gleich.
- CapSkip auf einer Windows-Maschine, erreichbar von der Maschine, die Ihre Tests ausführt.
- Die Grid-Adresse. Selenium 4 lauscht standardmäßig auf Port 4444 auf RemoteWebDriver-Anfragen.
- Der sitekey und die Seiten-URL der zu testenden Website.
Welche Maschine tatsächlich mit dem Löser spricht
Zeichnen Sie die drei Kästen auf, bevor Sie irgendetwas konfigurieren, denn zwei davon sehen austauschbar aus und sind es nicht.
| Welche Maschine | Was dort läuft | Spricht sie mit CapSkip? |
|---|---|---|
| Ihr Test-Runner | pytest, der SDK-Aufruf, RemoteWebDriver | Ja. Sie ist die einzige, die es tut |
| Der Grid-Hub | Routing und die Session-Queue | Nein. Er routet nur Sessions |
| Der Browser-Node | Der eigentliche Browser | Nein. Er sieht den Löser nie |
Erstaunlich oft wird das genau falsch herum verkabelt, meist weil die Grid-Nodes der Teil sind, der in Docker läuft, und man das Netzwerkproblem in Docker vermutet. Auf dem Node gibt es kein Netzwerkproblem. Das Token kommt dort über das gewöhnliche WebDriver-Protokoll an, als Argument eines Befehls zur Skriptausführung, genau wie jeder andere String auch.
Der Verbindungsmodus, den Sie brauchen, ergibt sich also daraus, wo Ihre Tests ausgeführt werden. CapSkip bietet Local, das sich an 127.0.0.1 bindet und nur dieses Gerät bedient, und Server, das sich an Ihre Netzwerkadresse oder öffentliche IP bindet, sodass eine andere Maschine, ein Container-Host oder ein CI-Runner dieselbe Windows-Maschine über die API erreicht. Beide finden Sie unter Verbindungseinstellungen. Der Server-Modus ändert nur, wo der Löser läuft, sonst nichts: Es ist weiterhin Ihre Hardware und weiterhin ohne Verbrauchsabrechnung.
| Wo Ihre Tests laufen | Welcher Modus, und der host-Wert |
|---|---|
| Auf Ihrer eigenen Windows-Maschine, die ein entferntes Grid steuert | Local-Modus. Der host-Wert bleibt 127.0.0.1, auch wenn der Browser woanders läuft |
| Auf einem Build-Agent im selben Netzwerk wie der Löser | Server-Modus. Der host-Wert ist die LAN-Adresse der Löser-Maschine |
| Auf einem gehosteten CI-Runner außerhalb Ihres Netzwerks | Server-Modus mit einer statischen öffentlichen IP, aktivierter Schlüsselprüfung und einer Firewall-Regel |
Die erste Zeile ist die bemerkenswerte. Wer Tests lokal gegen ein gemeinsam genutztes Grid ausführt, behält die Loopback-Adresse, denn die Lösung verlässt den eigenen Schreibtisch nie.
Schritt 1: den Driver auf das Grid richten
Selenium 4 nimmt die Grid-Adresse und ein Options-Objekt entgegen. Das alte Hub-Pfadsuffix wird nicht mehr gebraucht, die Basis-URL genügt.
# pip install selenium capskip
from selenium import webdriver
GRID = "http://grid.internal:4444"
options = webdriver.ChromeOptions()
options.add_argument("--no-sandbox")
# command_executor is the Grid, not the browser. Everything you
# call on this driver is a request over the wire to the node.
driver = webdriver.Remote(command_executor=GRID, options=options)Am Captcha ändert sich hier nichts. Was sich ändert: Jeder Driver-Aufruf ist jetzt ein Roundtrip, und das wird im Timeout-Abschnitt weiter unten relevant.
Schritt 2: lösen, dann das Token injizieren
Die Lösung ist ein lokaler Funktionsaufruf in Ihrem Testprozess. Die Injektion ist eine Skriptausführung auf dem Node. Halten Sie beides gedanklich auseinander, dann schreibt sich der Code von selbst.
# pip install capskip
import os
from capskip import CapSkip
# The host is 127.0.0.1 when the tests run on the solver machine,
# and the solver's address when they do not. It is never the node.
solver = CapSkip(
host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
port=8080,
)
result = solver.recaptcha(
sitekey="YOUR_SITEKEY",
url="https://example.com/page-with-recaptcha",
)
# This runs on the node. The token travels as a script argument.
driver.execute_script(
"document.getElementById('g-recaptcha-response')"
".value = arguments[0];",
result["code"],
)Das Token als Argument zu übergeben, statt den Skript-String darum herum zu bauen, ist auf einem Grid wichtiger als lokal, denn das Skript wird serialisiert und zum Node geschickt. Bei String-Verkettung werden aus Quoting-Fehlern stillschweigend leere Felder.
Jede reCAPTCHA-Variante ist dieselbe Methode mit einem zusätzlichen Keyword: invisible auf 1 gesetzt, enterprise auf 1 gesetzt oder version auf v3 gesetzt, dazu ein Action-Name. Turnstile und GeeTest sind eigene Methoden in derselben Form, und die Parameterliste steht in der CapSkip-API-Dokumentation.
Schritt 3: das Session-Timeout, das eine langsame Lösung abbricht
Hier kommt der Fehler, den es nur auf dem Grid gibt, und er ist ein guter. Ein Node beendet jede Session, die für die Dauer seines Session-Timeouts keine Aktivität hatte, standardmäßig 300 Sekunden. Während das SDK auf eine Antwort pollt, ruft Ihr Testprozess den Driver überhaupt nicht auf. Der Browser sitzt untätig da, und aus Sicht des Node sieht die Session verlassen aus.
Bei einer reCAPTCHA-v2-Checkbox kommt das nie vor, weil die Antwort meist deutlich unter einer Minute eintrifft. Es kommt bei Turnstile-Challenge-Seiten und GeeTest vor, bei einem ausgelasteten Löser und bei jedem Lauf, bei dem Sie zufällig an die eigene Obergrenze des SDK stoßen. Diese Obergrenze liegt bei 300 Sekunden, gesetzt durch recaptchaTimeout, und das ist genau der Standardwert des Node. Mit den Standardwerten können Sie dieses Rennen nicht gewinnen. Die Untätigkeitsuhr des Node startet, bevor das SDK zu pollen beginnt, die Session wird also zuerst abgeräumt, und der nächste Driver-Aufruf scheitert an einer ungültigen Session-id statt an irgendetwas, das mit Captchas zu tun hat.
Drei Abhilfen, in der Reihenfolge, in der es sich lohnt, sie auszuprobieren.
- Erhöhen Sie das Session-Timeout auf dem Node über dessen Session-Timeout-Option auf bequem mehr als 300 Sekunden. Das ist die ehrliche Lösung und sie kostet nichts.
- Halten Sie die Session beschäftigt, während die Lösung läuft. Der synchrone Client blockiert, das bedeutet also: die Lösung auf einen Thread auslagern oder den asynchronen Client verwenden und dann ab und zu einen günstigen Driver-Aufruf absetzen, um die Untätigkeitsuhr zurückzusetzen. Das funktioniert, um den Preis von mehr beweglichen Teilen.
- Senken Sie die eigene Obergrenze des SDK, damit es zuerst aufgibt, und lassen Sie Ihren Test es wiederholen. Eine TimeoutException aus einer Obergrenze, die Sie selbst gewählt haben, liest sich in einem Report besser als eine tote Session.
Zwei weitere Grid-Limits sollten Sie kennen, wenn Sie schon dabei sind. Die maximale Anzahl Sessions pro Node entspricht standardmäßig der Anzahl der Prozessoren auf diesem Node, und das deckelt Ihre tatsächliche Parallelität. Und eine neue Session-Anfrage, die länger als das Session-Request-Timeout in der Queue liegt, ebenfalls standardmäßig 300 Sekunden, wird abgelehnt, bevor überhaupt ein Browser startet.
Vollständiges lauffähiges Beispiel
Ein vollständiger Test, der die Seite öffnet, löst, injiziert und absendet. Das Absenden folgt mit Absicht direkt auf die Injektion: Ein reCAPTCHA-Token ist etwa zwei Minuten lang gültig, und ein Grid legt zwischen jeden Schritt zusätzliche Roundtrips. Mehr dazu im Leitfaden zu Gültigkeitsdauer von reCAPTCHA-Token.
# pip install selenium capskip
import os
from selenium import webdriver
from selenium.webdriver.common.by import By
from capskip import CapSkip
from capskip.exceptions import NetworkException, TimeoutException
GRID = "http://grid.internal:4444"
PAGE = "https://example.com/page-with-recaptcha"
def test_login_through_recaptcha():
options = webdriver.ChromeOptions()
driver = webdriver.Remote(command_executor=GRID, options=options)
try:
driver.get(PAGE)
# Read the sitekey off the rendered page rather than
# hardcoding it. It is on the node, so this is a round trip.
sitekey = driver.find_element(
By.CSS_SELECTOR, ".g-recaptcha"
).get_attribute("data-sitekey")
solver = CapSkip(host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"))
result = solver.recaptcha(sitekey=sitekey, url=PAGE)
driver.execute_script(
"document.getElementById('g-recaptcha-response')"
".value = arguments[0];",
result["code"],
)
driver.find_element(By.CSS_SELECTOR, "form").submit()
except NetworkException:
raise AssertionError("CapSkip unreachable from the test runner")
except TimeoutException:
raise AssertionError("Solve did not finish before the ceiling")
finally:
driver.quit()Beenden Sie den Driver auf einem Grid immer in einem finally-Block. Ein lokales Chrome, das leckt, ist ein verirrter Prozess auf Ihrer eigenen Maschine. Eine geleckte Grid-Session belegt einen der Session-Slots dieses Node, bis das Timeout sie abräumt, und auf einem Node mit vier Prozessoren ist bis dahin ein Viertel Ihrer Kapazität weg.
Lösungen über parallele Sessions hinweg ausführen
Der Sinn eines Grid ist, viele Browser gleichzeitig laufen zu lassen, und jede dieser Sessions braucht ein eigenes Token. Der Löser nimmt gleichzeitige Übermittlungen an, die Form, die funktioniert, hält die Lösung also asynchron, statt Ihre Suite hinter einem blockierenden Aufruf zu serialisieren.
Das Python SDK liefert dafür einen echten asynchronen Client mit, und das sei deutlich gesagt, weil die entsprechenden Klassen in den SDKs für Node.js, PHP und .NET Aliase sind und keine eigenen Implementierungen. Das Batching-Muster ist beschrieben in der Anleitung zum parallelen Lösen von Captchas mit Pythonund es gilt unverändert, wenn die Browser zufällig entfernt laufen.
Mehr davon laufen zu lassen kostet nichts zusätzlich. Eine Obergrenze hat dagegen das Grid: die maximale Anzahl Sessions pro Node und das Queue-Timeout davor. Bemessen Sie die Testparallelität am Grid, nicht am Löser.
Häufige Fehler und was sie bedeuten
| Was Sie sehen | Ursache | Beheben |
|---|---|---|
| NetworkException, Verbindung auf Port 8080 abgelehnt | Der Löser ist im Local-Modus und die Tests laufen auf einer anderen Maschine | Wechseln Sie in den Server-Modus und setzen Sie den Host auf die Adresse des Lösers |
| Sie haben den Node so konfiguriert, dass er den Löser erreicht, und nichts hat sich geändert | Der Node ruft CapSkip nie auf. Ihr Testprozess tut es | Richten Sie stattdessen den host-Wert vom Test-Runner aus auf den Löser |
| Ungültige Session-id direkt nach einer langen Lösung | Der Node hat eine untätige Session abgeräumt, während das SDK gepollt hat | Erhöhen Sie das Session-Timeout des Node auf über 300 Sekunden |
| Eine neue Session-Anfrage läuft ab, bevor ein Browser startet | Die Queue ist voll und die Anfrage ist verfallen | Fügen Sie Nodes hinzu oder erhöhen Sie das Session-Request-Timeout |
| Das Response-Feld ist leer, nachdem das Skript gelaufen ist | Das Token wurde in den Skript-String hineinverkettet und das Quoting ist kaputtgegangen | Übergeben Sie es als Skript-Argument, wie in den Beispielen oben |
| Ein gültiges Token wird von der Website abgelehnt | Es ist zwischen Lösung und Absenden abgelaufen | Senden Sie im nächsten Statement ab, ohne Wartezeiten dazwischen |
| ERROR_GOOGLEKEY beim Absenden | Der von der Seite gelesene sitekey war leer oder stammte aus dem falschen Element | Prüfen Sie den Selektor und ob das Widget gerendert war, bevor Sie ihn gelesen haben |
| Sessions stauen sich auf einem Node | Ein Test ist abgestürzt, bevor er den Driver beendet hat | Beenden Sie ihn in einem finally-Block, wie im vollständigen Beispiel |
FAQ
Muss ich auf den Grid-Nodes irgendetwas installieren?
Nein. Die Nodes führen Browser aus und sonst nichts. Das SDK ist eine Abhängigkeit Ihres Testprojekts, die Lösung passiert in Ihrem Testprozess, und das Einzige, was den Node erreicht, ist das Token, als Argument einer Skriptausführung. Genau deshalb funktioniert auch ein Linux-Docker-Node problemlos mit einem Löser, der nur unter Windows läuft: Die beiden sprechen nie miteinander.
Meine Tests laufen in CI. Was muss exponiert werden?
Der Port des Lösers, gegenüber der Maschine, die die Tests ausführt. Das bedeutet den Server-Modus mit einer statischen öffentlichen IP, wenn der Runner gehostet ist, dazu eine aktivierte Prüfung des API-Schlüssels und eine Firewall-Regel, die enger ist als das gesamte Internet. Sind Ihre CI-Runner selbst gehostet und stehen in Ihrem eigenen Netzwerk, genügt die LAN-Adresse und nichts muss es verlassen. So oder so bleibt das Grid unberührt, denn es ist nicht Teil dieses Wegs.
Warum ist meine Session mitten in einer Turnstile-Lösung gestorben?
Weil der Node für die Dauer seines Session-Timeouts keine Aktivität gesehen und aufgeräumt hat. Der Standardwert liegt bei 300 Sekunden, und die eigene Obergrenze des SDK für Turnstile liegt ebenfalls bei 300 Sekunden, und die Untätigkeitsuhr des Node startet zuerst, bei diesen Standardwerten räumt der Node die Session also immer ab, bevor das SDK aufgibt. Erhöhen Sie das Session-Timeout des Node und überlegen Sie, die Obergrenze des SDK zu senken, damit der Fehlschlag als saubere Exception zurückkommt und nicht als tote Session.
Ist das anders als das Lösen mit einem lokalen Driver?
Nur darin, wo die Dinge laufen. Der Lösungsaufruf und die Injektion sind identisch, und genau darum geht es. Die Unterschiede sind betrieblicher Natur: das Timeout des Node für untätige Sessions, der Roundtrip bei jedem Driver-Aufruf und die Tatsache, dass die Adresse des Lösers vom Standort Ihres Test-Runners bestimmt wird. Die Varianten mit einem einzelnen Browser werden behandelt in dem SeleniumBase-Leitfaden und dem undetected-chromedriver-Leitfaden.
Die Kurzfassung
Auf einem Grid zieht der Browser um und Ihr Code nicht, die Adresse des Lösers wird also davon bestimmt, wo Ihre Tests laufen. Richten Sie RemoteWebDriver auf Port 4444, rufen Sie das SDK aus dem Testprozess auf und übergeben Sie das Token als Skript-Argument an den Node, statt einen String zu bauen. Erhöhen Sie das Session-Timeout des Node auf über 300 Sekunden, damit eine langsame Lösung ihre Session nicht abgeräumt bekommt, senden Sie direkt nach dem Injizieren ab und beenden Sie den Driver immer in einem finally-Block.
- Das Framework als Ganzes wird behandelt auf die Selenium-Captcha-Solver-Seite.
- Die Python-Client-Bibliothek wird behandelt auf der Python-Captcha-Solver-Seite.
- Die Challenge selbst wird behandelt auf die reCAPTCHA-v2-Solver-Seite.
Gut zu wissen, bevor Sie die Test-Suite breiter aufstellen: CapSkip ist ein Captcha-Löser der auf Hardware läuft, die Ihnen bereits gehört, hundert parallele Sessions kosten also genau dasselbe wie eine.
