Captchas in einem Windmill-Skript lösen (Python)

Kleiner als eine Captcha-Lösung in Windmill wird diese Integration nicht. Windmill liest die Imports am Anfang Ihres Skripts, löst sie gegen PyPI auf und pinnt sie in einem Lockfile, das CapSkip SDK kommt also ohne Installationsschritt und ohne requirements-Datei an. Übrig bleibt eine main-Funktion, die einen sitekey entgegennimmt und Ihnen ein Token zurückgibt. Nachdenken müssen Sie nicht über den Code, sondern darüber, auf welcher Maschine der Worker läuft, denn das entscheidet, ob der Löser auf der Loopback-Adresse bleibt oder in Ihrem Netzwerk lauschen muss.
Was Sie brauchen
- Eine Windmill-Instanz, selbst gehostet oder in deren Cloud, und ein Workspace, in den Sie ein Skript deployen können.
- CapSkip auf einer Windows-Maschine. Der Local-Modus genügt, wenn der Worker auf derselben Maschine läuft. Leben die Worker in Containern oder auf einem anderen Host, wechseln Sie in den Server-Modus.
- Der sitekey und die Seiten-URL der Website, die Sie automatisieren.
- Eine Windmill-Variable, die den Schlüssel des Lösers enthält, und eine Worker-Umgebungsvariable, die seine Adresse enthält.
Warum die Import-Zeile die gesamte Installation ist
Windmill parst beim Speichern eines Skripts die Top-Level-Imports, ermittelt, auf welche PyPI-Pakete sie abbilden, und startet einen Dependency-Job, der ein Lockfile schreibt. Dieses Lockfile hängt an der jeweiligen Version des Skripts, das Deployment, das Sie getestet haben, ist also auch das Deployment, das sechs Monate später läuft. Es gibt keine requirements-Datei zu pflegen und nichts, was Sie von Hand auf dem Worker installieren müssten.
An derselben Stelle können Sie auch den Interpreter pinnen, mit einem Kommentar im Skript-Header. Ein deploytes Skript, das keine Version anfordert, läuft auf Python 3.11.
# py312 # pip install capskip - Windmill resolves this import itself # and locks the version when the script is deployed. from capskip import CapSkip
Schritt 1: das Lösungsskript
Ein Windmill-Skript ist eine main-Funktion. Ihre Argumente werden zum Input-Schema und zu dem Formular, das Windmill rendert, typisieren Sie sie also. Was immer Sie zurückgeben, ist das Ergebnis des Skripts, und ein nachgelagerter Flow-Step liest es dort aus.
# py312
# pip install capskip - resolved from this import on save.
import wmill
from capskip import CapSkip
def main(sitekey: str, page_url: str) -> str:
# Host and port come from the worker environment. The key is
# a Windmill variable, so it is stored encrypted and never
# appears in the script body or in the run logs.
solver = CapSkip(
host=wmill.get_variable("u/admin/capskip_host"),
port=8080,
apiKey=wmill.get_variable("u/admin/capskip_key"),
)
result = solver.recaptcha(sitekey=sitekey, url=page_url)
return result["code"] # the token, for the next stepDas ist die gesamte Integration für reCAPTCHA v2. Jede andere 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 haben eigene Methoden in derselben Form, und die vollständige Parameterliste steht in der CapSkip-API-Dokumentation.
Zwei Dinge über das SDK sollten Sie wissen, bevor Sie zu einer handgeschriebenen Polling-Schleife greifen. Es übernimmt das Polling für Sie, startet bei 250 Millisekunden und nimmt den Takt zurück, statt ein festes Intervall zu schlafen, was die veröffentlichten Wartezeiten der rohen API meist schlägt. Und seine Obergrenze für reCAPTCHA, Turnstile und GeeTest liegt bei 300 Sekunden, gesetzt durch recaptchaTimeout. Diese Zahl ist wichtig, wenn Sie in Schritt 4 das Skript-Timeout setzen.
Schritt 2: den Schlüssel in einer Windmill-Variable halten
Windmill hat vollwertige Variablen und Secrets, und das Skript oben liest eine davon direkt aus. Es gibt einen zweiten Weg, der besser zu Flows passt: Übergeben Sie die Variable über die Referenzsyntax als Step-Argument, und Windmill löst sie zur Laufzeit mit den Rechten des Aufrufers auf.
| Wo der Wert liegt | Wie das Skript an ihn kommt |
|---|---|
| Eine Windmill-Secret-Variable | Im Skriptrumpf mit dem wmill-Client auslesen, wie oben |
| Eine Windmill-Variable, als Step-Argument übergeben | Geben Sie dem Argument den Wert dollar-var, gefolgt vom Variablenpfad |
| Eine Windmill-Resource, die mehrere Felder auf einmal enthält | Geben Sie dem Argument den Wert dollar-res, gefolgt vom Resource-Pfad |
| Eine Umgebungsvariable auf dem Worker-Host | Aus der Prozessumgebung auslesen, sobald der Worker sie durchreichen darf |
Diese Referenzen werden rekursiv aufgelöst, auch innerhalb von Listen und verschachtelten Objekten, ein Step, der eine Liste von Schlüsseln entgegennimmt, kann also in jedem Element eine Referenz halten. Geben Sie diesem Workspace einen eigenen Löser-Schlüssel, statt einen einzigen über alles zu teilen, was Sie betreiben.
Schritt 3: Wo der Worker läuft, entscheidet über den Verbindungsmodus
Das ist die Frage, die das Setup tatsächlich formt, und sie wird leicht falsch beantwortet, weil das Skript in beiden Fällen identisch aussieht. Ein Windmill-Worker ist ein eigenständiger Prozess, der jeweils ein Skript ausführt. Das kann ein Container neben der Datenbank sein, ein Prozess auf einer VM oder ein Prozess auf Ihrem eigenen Desktop. Was immer es ist: Der SDK-Aufruf öffnet einen Socket vom Worker aus, der Löser muss also von dort erreichbar sein und von sonst nirgends.
CapSkip hat genau dafür zwei Verbindungsmodi. Local bindet sich an 127.0.0.1 und bedient nur dieses Gerät. Server bindet sich an Ihre Netzwerkadresse oder öffentliche IP, sodass eine andere Maschine, ein Container-Host oder eine gehostete Plattform dieselbe Windows-Maschine über die API erreicht. Beide werden eingerichtet unter Verbindungseinstellungen, und der Server-Modus ändert nur, wo der Löser läuft. Es ist weiterhin Ihre Hardware und weiterhin ohne Verbrauchsabrechnung.
| Wo Ihr Worker läuft | Welcher Modus, und der host-Wert |
|---|---|
| Auf derselben Windows-Maschine wie CapSkip | Local-Modus. Der host-Wert bleibt 127.0.0.1 |
| In einem Container oder auf einer anderen Maschine im eigenen Netzwerk | Server-Modus. Der host-Wert ist die LAN-Adresse der Löser-Maschine |
| In der Cloud von Windmill oder auf einer VM außerhalb Ihres Netzwerks | Server-Modus mit einer statischen öffentlichen IP, dazu eine Firewall-Regel |
Windmill-Worker laufen durchaus auf Windows, und genau das ist der Fall, der Sie auf der Loopback-Adresse hält. Dort ist eine Einstellung wichtig. Die PID-Namespace-Isolierung steht unter Linux standardmäßig auf true, und die Dokumentation von Windmill sagt, dass sie für Windows-Worker auf false gesetzt werden soll. Setzen Sie auf einem Windows-Worker die Variable ENABLE_UNSHARE_PID auf false, und er startet normal.
Für die anderen beiden Zeilen gehört die Adresse in die Worker-Umgebung und nicht ins Skript. Windmill reicht nicht standardmäßig jede Host-Variable an einen Job weiter, benennen Sie die gewünschten also kommagetrennt in der Variable WHITELIST_ENVS auf dem Worker. Eine Worker-Gruppe kann außerdem eigene statische und dynamische Umgebungsvariablen tragen, die Sie in der UI setzen, was die sauberere Option ist, wenn nur ein Teil Ihrer Worker in der Nähe des Lösers sitzt.
Schritt 4: das Timeout und der Retry
Windmill stellt in den Runtime-Settings eines Skripts ein Feld Timeout bereit, neben Cache und Concurrency-Limits. Setzen Sie es über Ihre langsamste Lösung, nicht darunter. Eine reCAPTCHA-v2-Checkbox landet meist deutlich unter einer Minute, Turnstile-Challenge-Seiten und GeeTest brauchen jedoch länger, und das SDK pollt bis zu 300 Sekunden, bevor es mit einer TimeoutException aufgibt. Ein Skript-Timeout unterhalb dieses Werts macht aus einer langsamen Lösung einen abgebrochenen Job, in dessen Log nichts Verwertbares steht.
Sobald das Skript ein Step in einem Flow ist, kommt eine zweite Ebene dazu. Windmill-Flow-Steps wiederholen sich in zwei Formen, und die exponentielle passt zu einem Löser, der kurz beschäftigt ist.
| Form des Retry | Was Sie konfigurieren | Wann Sie sie einsetzen |
|---|---|---|
| Eine konstante Verzögerung | Eine maximale Anzahl an Versuchen und eine feste Verzögerung | Ein Löser, der gelegentlich neu startet, wo eine gleichbleibende Wartezeit reicht |
| Exponentielles Backoff | Eine maximale Anzahl an Versuchen, eine Basis in Sekunden und ein Multiplikator | Alles, was wirklich ausgelastet sein könnte, damit Sie zurückweichen, statt darauf einzuhämmern |
Die Verzögerung der exponentiellen Form ist der Multiplikator mal die Basis hoch der Versuchsnummer, eine Basis von 3 mit einem Multiplikator von 2 verteilt die Wartezeiten über fünf Versuche also von 6 Sekunden bis auf 486. Es gibt außerdem die Einstellung Continue on error, mit der der Flow weiterläuft, nachdem die Retries aufgebraucht sind, und den Fehler als Step-Ergebnis weiterreicht, und genau so bauen Sie einen Zweig, der zurückfällt, statt den Lauf scheitern zu lassen.
Vollständiges lauffähiges Beispiel
Ein Skript, das löst und absendet, damit das Token nie herumliegt und auf den nächsten Step wartet. Der letzte Punkt ist keine Stilfrage. Ein reCAPTCHA-Token ist etwa zwei Minuten lang gültig, und ein Flow, der in einem Step löst, auf eine Freigabe wartet und dann in einem anderen absendet, ist der zuverlässige Weg, es zu verlieren. Mehr dazu steht im Leitfaden zu Gültigkeitsdauer von reCAPTCHA-Token.
# py312
# pip install capskip requests - both resolved from these imports.
import os
import requests
import wmill
from capskip import CapSkip
from capskip.exceptions import TimeoutException, NetworkException
SITE = "https://example.com/page-with-recaptcha"
def main(sitekey: str, username: str) -> dict:
# CAPSKIP_HOST is set on the worker and allowed through by
# WHITELIST_ENVS. It falls back to the loopback address so the
# same script still runs on a worker that sits next to CapSkip.
solver = CapSkip(
host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
port=8080,
apiKey=wmill.get_variable("u/admin/capskip_key"),
)
try:
result = solver.recaptcha(sitekey=sitekey, url=SITE)
except TimeoutException:
# Let the flow's retry policy decide what happens next.
raise
except NetworkException:
raise RuntimeError("CapSkip is unreachable from this worker")
# Submit immediately. The token is short lived, and the field
# name below is the one the page's own form posts.
posted = requests.post(
SITE,
data={
"username": username,
"g-recaptcha-response": result["code"],
},
timeout=30,
)
return {"status": posted.status_code, "captcha_id": result["captchaId"]}Dass das letzte Skript die Captcha-id statt des Tokens zurückgibt, ist Absicht. Die id ist nützlich, wenn Sie später die Ausführungshistorie von Windmill lesen, das Token nicht: Es ist dann längst abgelaufen, und es in ein gespeichertes Job-Ergebnis zu schreiben bedeutet, es in Ihre Logs zu schreiben.
Mehrere gleichzeitig lösen
Ein Windmill-Worker führt jeweils ein Skript aus und nutzt dafür die ganze Maschine, die er hat. Nebenläufigkeit ist hier also eine Frage davon, wie viele Worker Sie betreiben, und nicht davon, was Ihr Skript tut. Zwei Wege dorthin, und sie lassen sich kombinieren.
- Betreiben Sie mehr Worker. Eine Worker-Gruppe lässt sich unabhängig skalieren, und Jobs werden von dem Worker übernommen, der gerade frei ist.
- Lösen Sie im Batch innerhalb eines Skripts. Das Python SDK liefert einen echten asynchronen Client mit, mehrere Lösungen können also in einem einzigen Job gleichzeitig unterwegs sein. Das lohnt sich, wenn ein Lauf zehn Token statt einem braucht, und das Muster ist beschrieben in der Anleitung zum parallelen Lösen von Captchas mit Python.
Setzen Sie ein Concurrency-Limit auf das Skript, wenn die Zielseite der empfindliche Teil ist. CapSkip selbst rechnet nicht nach Verbrauch ab, mehr davon laufen zu lassen kostet also nichts zusätzlich, aber der Website, die Sie automatisieren, kann es durchaus auffallen.
Häufige Fehler und was sie bedeuten
| Was Sie sehen | Ursache | Beheben |
|---|---|---|
| NetworkException, Verbindung auf Port 8080 abgelehnt | Der Worker läuft nicht auf der Maschine, an die CapSkip gebunden ist | Wechseln Sie in den Server-Modus und setzen Sie die host-Variable auf die Adresse des Lösers |
| Die host-Umgebungsvariable ist innerhalb des Jobs leer | Sie existiert auf dem Worker, wurde aber nie durchgelassen | Tragen Sie ihren Namen in WHITELIST_ENVS ein oder setzen Sie sie auf der Worker-Gruppe |
| ModuleNotFoundError beim capskip-Import | Der Dependency-Job ist für diese Version noch nicht gelaufen | Speichern und deployen Sie das Skript und prüfen Sie dann, ob der Dependency-Job fertig geworden ist |
| Der Job wird mitten in einer Lösung abgebrochen | Das Skript-Timeout ist kürzer als die Lösung gedauert hat | Erhöhen Sie Timeout in den Runtime-Settings des Skripts auf über 300 Sekunden |
| TimeoutException aus dem SDK | Die Lösung hat recaptchaTimeout tatsächlich überschritten | Lassen Sie den Flow es wiederholen und prüfen Sie, ob sitekey und Seiten-URL stimmen |
| ERROR_WRONG_USER_KEY in der Antwort | Die Windmill-Variable ist leer, es wurde also ein leerer Schlüssel gesendet | Prüfen Sie den Variablenpfad, einschließlich des Workspace-Präfixes |
| Ein Worker unter Windows startet nicht | Die PID-Namespace-Isolierung wurde für diesen Worker nicht abgeschaltet | Setzen Sie ENABLE_UNSHARE_PID auf diesem Worker auf false |
| Ein gültiges Token wird von der Zielwebsite abgelehnt | Es ist zwischen dem Lösungs-Step und dem Absende-Step abgelaufen | Lösen und senden Sie in einem Skript ab, oder in direkt benachbarten Steps ohne Wartezeit |
FAQ
Kann ich CapSkip mit Windmill auf der Loopback-Adresse lassen?
Ja, wenn der Worker auf derselben Windows-Maschine läuft wie der Löser. Das ist das eine Deployment, in dem der Local-Modus überlebt, und es lohnt sich, das bewusst einzurichten: Betreiben Sie einen eigenen Worker auf der Löser-Maschine, geben Sie ihm ein eigenes Worker-Tag und leiten Sie die Captcha-Skripte an dieses Tag. Jede andere Form, einschließlich der Cloud von Windmill und jedes Containers, braucht den Server-Modus, weil der Worker woanders sitzt.
Brauche ich für das SDK eine requirements-Datei?
Nein. Windmill liest beim Speichern des Skripts die Top-Level-Imports, ordnet sie PyPI-Paketen zu und erzeugt ein Lockfile für diese Version des Skripts. Die Import-Zeile ist die Abhängigkeitsdeklaration. Schlägt der Import zur Laufzeit fehl, sollten Sie nachsehen, ob der Dependency-Job durchgelaufen ist, und nicht, ob eine Datei fehlt.
Sollte der Flow das Ergebnis selbst pollen?
Nicht bei einer Lösung, die in einen Job passt. Das SDK pollt bereits, und es beginnt bei 250 Millisekunden und nimmt den Takt zurück, statt ein festes Intervall zu schlafen, eine selbstgebaute Schleife aus Sleep-Steps ist also langsamer und mehr Code. Pollen Sie nur dann aus dem Flow heraus, wenn Sie Absenden und Abholen bewusst auf zwei Steps aufgeteilt haben, und prüfen Sie in diesem Fall die Warteantwort namentlich. Sie wird CAPCHA_NOT_READY geschrieben, ohne das T, und sie bedeutet weiter warten und nicht, dass etwas schiefgegangen ist. Es gibt eine ausführliche Beschreibung der CAPCHA_NOT_READY-Antwort.
Wie schneidet das gegenüber Airflow, Dagster oder n8n ab?
Windmill braucht von den vieren den wenigsten Code, weil ein Skript eine schlichte Funktion ist und seine Abhängigkeiten aus der Import-Zeile kommen. Airflow will einen Task innerhalb eines DAG und bringt ein Scheduler-Intervall mit, über das Sie nachdenken müssen, behandelt wird das in dem Leitfaden zum Airflow-Captcha-DAG. Dagster fasst dieselbe Arbeit als Asset auf, beschrieben in der Dagster-Anleitung. n8n ist ein Node-Graph und keine Code-Laufzeitumgebung, und der n8n-Leitfaden ist um dessen HTTP Request-Node herum aufgebaut.
Die Kurzfassung
Importieren Sie das SDK am Anfang eines Windmill-Skripts und lassen Sie den Dependency-Job es pinnen. Legen Sie den Löser-Schlüssel in eine Windmill-Variable und seine Adresse in eine Worker-Umgebungsvariable, die WHITELIST_ENVS durchlässt. Entscheiden Sie den Verbindungsmodus danach, wo der Worker läuft, nicht danach, wo Sie sitzen: Ein Worker auf der Windows-Maschine des Lösers behält den Local-Modus, alles andere braucht den Server-Modus. Setzen Sie das Skript-Timeout über 300 Sekunden, ergänzen Sie exponentielles Backoff am Flow-Step und senden Sie das Token in demselben Job ab, der es erzeugt hat.
- Das Python SDK selbst wird behandelt auf der Python-Captcha-Solver-Seite.
- Die Checkbox-Challenge wird behandelt auf die reCAPTCHA-v2-Solver-Seite.
- Die entsprechenden Aufrufe in Node.js, PHP und C# sind aufgeführt auf Die Seite zu den SDKs fürs Captcha-Lösen.
Eines sollten Sie abwägen, bevor Sie das alle fünf Minuten einplanen: CapSkip ist ein lokaler Captcha-Löser der auf Hardware läuft, die Ihnen bereits gehört, ein Flow, der ständig feuert, kostet also genau dasselbe wie einer, der nur gelegentlich feuert.
