Captchas in einem Celery-Task lösen (Python-Queue)

celery captcha - How to Solve CAPTCHAs in a Celery Task (Python Queue)

Ein Celery-Captcha-Task hat eine Regel, die alles andere prägt: Er muss bei jedem Versuch von vorn lösen. Celery liefert mindestens einmal zu, ein Task kann also zweimal laufen, und ein CapSkip-Ergebnis ist nur einmal lesbar. Wenn Sie eine Captcha-id speichern und nach einem Retry darauf aufsetzen, bekommen Sie nichts zurück. Lösen, den Token verwenden und fertig werden, alles innerhalb eines einzigen Task-Körpers.

Was Sie brauchen

  • Celery 5 mit einem Broker, Redis oder RabbitMQ. Die Wahl des Brokers ändert später eine Einstellung.
  • CapSkip läuft auf einem Windows-Rechner, der Python-Client ist im Worker-Image installiert.
  • Server-Modus, in fast jedem echten Deployment. Worker laufen meist in Linux-Containern, der Solver nicht.
  • Der sitekey und die Seiten-URL, als Task-Argumente übergeben statt fest im Task verdrahtet.
# pip install capskip
pip install -U celery[redis] capskip

Schritt 1: der Task

Der gesamte Lösevorgang ist ein einziger Aufruf. Das SDK sendet ab, fragt ab und gibt den Token zurück, es gibt also keine id, die zwischen Schritten weitergereicht werden müsste, und nichts zu speichern.

# pip install capskip
import os
from celery import Celery
from capskip import CapSkip, NetworkException

app = Celery("solves", broker="redis://redis:6379/0")

# CAPSKIP_HOST is the solver machine. Loopback only works if the
# worker runs on the same Windows box as CapSkip.
solver = CapSkip(
    host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
    port=int(os.environ.get("CAPSKIP_PORT", "8080")),
    apiKey=os.environ.get("CAPSKIP_API_KEY", "capskip"),
)

@app.task(
    bind=True,
    autoretry_for=(NetworkException,),
    retry_backoff=True,
    max_retries=3,
    soft_time_limit=330,
    time_limit=360,
)
def solve_and_submit(self, sitekey, page_url):
    result = solver.recaptcha(sitekey=sitekey, url=page_url)
    return submit_form(page_url, result["code"])   # token, used here

Achten Sie darauf, was dort nicht steht. Keine Captcha-id im Rückgabewert, kein zweiter Task zum Absenden des Tokens, kein für später gespeichertes Ergebnis. Der Token wird in demselben Task verwendet, der ihn erzeugt hat. Der Grund ist das Timing und nicht die Ordnungsliebe, und er wird unten bei den Retries behandelt.

Dieser Aufruf ist reCAPTCHA v2. Die anderen Typen, die CapSkip unterstützt, haben dieselbe Form: invisible oder enterprise auf 1 übergeben, oder version auf v3 mit einer Action, oder stattdessen turnstile oder geetest aufrufen. Der vollständige Umfang steht auf der Python-Captcha-Solver-Seite.

Schritt 2: zwei Zeitlimits, und wohin damit

Celery hat standardmäßig kein Zeitlimit, bei keiner der beiden Einstellungen. Das ist der falsche Standard für einen Task, der auf einen Netzwerkdienst wartet, denn ein hängender Lösevorgang belegt einen Worker-Slot für immer.

Setzen Sie beide, und setzen Sie sie höher als das, was das SDK selbst zulässt. CapSkip gibt einem reCAPTCHA-, Turnstile- oder GeeTest-Lösevorgang dreihundert Sekunden und einem Bild-Captcha hundertzwanzig, beides am Client konfigurierbar. Wenn das Soft Limit zuerst greift, löst Celery in Ihrem Task SoftTimeLimitExceeded aus und Sie verlieren die TimeoutException des SDK, die das nützlichere Signal ist, weil sie Ihnen sagt, dass der Solver erreicht wurde und nicht fertig geworden ist.

Welche EinstellungEmpfohlener Wert für einen LösevorgangWarum
soft_time_limit330 SekundenDreißig Sekunden über der Obergrenze des Solvers, damit das SDK zuerst meldet
time_limit360 SekundenDie Rückfallebene. Der Worker beendet an dieser Stelle den Prozess
recaptchaTimeout am Client300 Sekunden, der StandardwertSenken Sie ihn, wenn Sie lieber schnell scheitern als warten
defaultTimeout am Client120 Sekunden, der StandardwertNur Bild-Captchas. Sie brauchen selten Sekunden, geschweige denn Minuten

Wenn Sie Bild-Captchas und reCAPTCHA über denselben Worker laufen lassen, geben Sie ihnen getrennte Tasks mit getrennten Limits statt eines einzigen Tasks mit dem höheren Paar. Eine Obergrenze von dreihundert Sekunden bei einem Job, der normalerweise in einer Sekunde fertig ist, verdeckt echte Fehler fünf Minuten lang.

Schritt 3: die Retry-Regel, die speziell fürs Lösen gilt

Die automatischen Retries von Celery sind genau richtig für einen Solver, der gerade neu startet, und genau falsch für einen Token, den Sie bereits halten. Bei diesem Unterschied lohnt sich Genauigkeit.

Mit retry_backoff auf True wartet der erste Retry eine Sekunde, dann zwei, dann vier, dann acht, und Jitter ist standardmäßig aktiv, die tatsächliche Verzögerung ist also ein Zufallswert bis zu diesem Maximum. Die Obergrenze ist retry_backoff_max mit einem Standardwert von sechshundert Sekunden. Vergleichen Sie das mit einem reCAPTCHA-Token, das etwa zwei Minuten gültig ist.

Ein Task, der erfolgreich gelöst, den Token gespeichert, beim Absenden versagt und dann einen Retry ausgelöst hat, kann also nach einer Verzögerung fortgesetzt werden, die um ein Vielfaches länger ist als die Lebensdauer des Tokens. Er scheitert an einem Token, der bei seiner Erstellung völlig gültig war, und das Log gibt der Zielseite die Schuld. Mehr zu diesem Fehlerfall steht im Leitfaden zu Gültigkeitsdauer von reCAPTCHA-Token.

Die Lösung ist die Task-Form von oben: innerhalb des Retrys lösen, nicht davor.

Beschränken Sie autoretry_for auf die Exceptions, die ein Transportproblem beschreiben. NetworkException heißt, dass CapSkip nicht erreichbar war, und das ist einen Retry wert. ApiException und ValidationException heißen, dass die Anfrage falsch war und wieder falsch sein wird. TimeoutException ist Ermessenssache und meist eher einen Retry wert als drei.

Schritt 4: acks_late, und warum ein Ergebnis nur einmal gelesen werden kann

Standardmäßig bestätigt Celery eine Nachricht kurz vor dem Ausführen, ein Worker, der mitten im Task stirbt, verliert den Job also. Mit aktiviertem task_acks_late wandert die Bestätigung hinter das Ende des Tasks, der Job eines abgestürzten Workers wird also erneut zugestellt und läuft noch einmal. Das ist meist das, was Sie bei Arbeit wollen, die anderswo Geld kostet, und genau deshalb wird die Regel vom einmaligen Lesen wichtig.

Ein CapSkip-Ergebnis ist nur einmal lesbar. Wenn der erste Versuch die Challenge abgesendet, den Token gelesen und dann vor der Bestätigung abgestürzt ist, kann der erneut zugestellte Versuch diese id nicht noch einmal lesen. Er muss erneut absenden. Genau das tut der Task oben, weil er zwischen den Versuchen keinen Zustand hält, und das erneute Lösen kostet ein paar Sekunden Ihrer eigenen Hardware statt einer zweiten Abrechnung.

# Redelivery is safe here because the task resolves rather
# than resuming. Pair it with reject_on_worker_lost so a
# killed worker requeues instead of dropping the job.
app.conf.task_acks_late = True
app.conf.task_reject_on_worker_lost = True

# Long tasks and a prefetch of 4 means idle workers sit on
# queued jobs. Drop it to 1 for solve queues.
app.conf.worker_prefetch_multiplier = 1

Der letzte Punkt ist der stille Performance-Fehler in den meisten Löse-Queues. Der Standard-Prefetch-Multiplikator ist vier, jeder Worker-Prozess reserviert also vorab vier Nachrichten. Bei Tasks, die Millisekunden dauern, ist das ein Gewinn. Bei Tasks, die auf einen Lösevorgang warten, liegen drei dieser vier reserviert hinter einem Job, der nichts tut außer abzufragen, während ein anderer Worker nichts zu tun hat.

Schritt 5: die Broker-Einstellung, die Lösevorgänge verdoppelt

Wenn Ihr Broker Redis ist, kommt noch eine Zahl dazu. Redis kennt keine native Bestätigung, Celery bildet sie also mit einem Visibility Timeout nach: der Anzahl Sekunden, die es auf die Bestätigung eines Tasks durch einen Worker wartet, bevor es die Nachricht an einen anderen Worker weiterreicht. Der Standardwert ist eine Stunde, und er steht in broker_transport_options, statt eine eigene Einstellung zu sein.

Eine Stunde liegt bequem über einem Lösevorgang von dreihundert Sekunden, der Standardwert ist also sicher. Ärger beginnt, wenn jemand ihn senkt, damit fehlgeschlagene Jobs schneller wieder anlaufen, denn die Celery-Dokumentation warnt selbst, dass ein Task, dessen Ausführungszeit das Visibility Timeout überschreitet, immer wieder ausgeführt wird, in einer Schleife. Jeder langsame Lösevorgang läuft dann mindestens zweimal.

# Keep this above your hard time limit, not near it.
# 3600 is the default and it is fine. If you must lower it,
# stay well clear of the 360 second time_limit above.
app.conf.broker_transport_options = {"visibility_timeout": 3600}

RabbitMQ bestätigt nativ und hat keine entsprechende Einstellung, das ist ein Grund, es für Queues voller langsamer Tasks zu bevorzugen.

Schritt 6: den Solver dort betreiben, wo die Worker ihn erreichen

Celery-Worker laufen meist in Linux-Containern auf einem Cluster. CapSkip läuft unter Windows. In der Praxis liegen Worker und Solver also auf verschiedenen Rechnern, und Loopback ist nicht die Antwort.

CapSkip hat dafür zwei Verbindungsmodi. Local bindet an 127.0.0.1 und bedient nur dieses Gerät. Server bindet an Ihre Netzwerkadresse oder öffentliche IP, sodass ein anderer Rechner, ein Container-Host oder eine gehostete Plattform denselben Windows-Rechner über die API erreichen kann. Beide finden Sie unter Verbindungseinstellungen, und der Server-Modus ändert nur, auf welcher Adresse der Löser lauscht. Es ist weiterhin Ihre Hardware, und es wird weiterhin nicht pro Lösung abgerechnet.

Dieser letzte Punkt ist es, der eine ständig feuernde Queue überhaupt vertretbar macht.

Wo die Worker laufenWelcher Verbindungsmodus
Auf derselben Windows-Maschine wie CapSkipLocal-Modus, der Host bleibt 127.0.0.1
In Docker oder auf einem anderen Rechner in Ihrem NetzwerkServer-Modus mit der LAN-Adresse des Solvers
Auf einer verwalteten Plattform oder einem Cloud-ClusterServer-Modus mit einer statischen öffentlichen IP und einer Firewallregel

Lesen Sie Host und Schlüssel aus der Umgebung statt aus dem Code. Der Python-Client liest weder CAPSKIP_HOST noch CAPSKIP_PORT noch CAPSKIP_API_KEY von sich aus, deshalb liest der Task in Schritt 1 diese Variablen aus und übergibt sie. Ein Worker-Container braucht dann genau diese Variablen und sonst nichts.

Viele gleichzeitig lösen

Zwei Wege, und sie passen zu unterschiedlichen Arten von Arbeit. Ein Task pro Captcha, wobei die Worker-Concurrency die Parallelität übernimmt, ist die normale Antwort, und darum geht es in dem Prefetch-Hinweis oben. Für einen Stapel, der gemeinsam ankommt, hat der Python-Client eine echte async-Implementierung, ein einzelner Task kann einen Stapel also in sich selbst zusammenführen.

import asyncio
import os
from capskip import AsyncCapSkip

# AsyncCapSkip in Python is a real async client, not an alias.
async def solve_batch(pairs):
    solver = AsyncCapSkip(
        host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"), port=8080)
    return await asyncio.gather(*[
        solver.recaptcha(sitekey=k, url=u) for k, u in pairs
    ])

@app.task(soft_time_limit=330, time_limit=360)
def solve_many(pairs):
    return [r["code"] for r in asyncio.run(solve_batch(pairs))]

Gut zu wissen, bevor Sie das in eine andere Sprache übertragen: Die Node- und .NET-Clients benennen ebenfalls eine Klasse AsyncCapSkip, dort ist sie aber ein Alias und keine zweite Implementierung. Python ist die Sprache, in der das etwas bedeutet. Der vollständige Vergleich steht im Leitfaden zum parallelen Lösen von Captchas in Python.

Häufige Fehler und was sie bedeuten

Was Sie sehenUrsacheBeheben
NetworkException bei jedem TaskCapSkip ist an Loopback gebunden und der Worker läuft woandersWechseln Sie in den Server-Modus und setzen Sie CAPSKIP_HOST auf die Adresse des Solvers
SoftTimeLimitExceeded statt TimeoutExceptionDas Soft Limit liegt unter der Obergrenze des ClientsErhöhen Sie soft_time_limit über 300, oder senken Sie recaptchaTimeout
Derselbe Job läuft bei einem langsamen Lösevorgang zweimalDas Visibility Timeout von Redis ist kürzer als der TaskErhöhen Sie es deutlich über das harte Zeitlimit
Ein Retry scheitert an einem abgelaufenen TokenDer Token wurde vor dem Retry gelöst, nicht darinVerlegen Sie den Lösevorgang in den Task-Körper, wie oben
Das Lesen derselben Captcha-id liefert nichtsEin CapSkip-Ergebnis ist nur einmal lesbarSpeichern Sie eine id nie über Versuche hinweg. Lösen Sie stattdessen neu
ERROR_WRONG_USER_KEY in einer ApiExceptionCAPSKIP_API_KEY ist in der Worker-Umgebung nicht gesetztSetzen Sie ihn in der Worker-Umgebung und starten Sie den Worker neu
Untätige Worker, während sich eine Queue stautPrefetch reserviert lange Tasks hinter langen TasksSetzen Sie worker_prefetch_multiplier bei Löse-Queues auf 1
Jobs verschwinden, wenn ein Worker beendet wirdDie späte Bestätigung ist ausgeschaltetSchalten Sie task_acks_late und task_reject_on_worker_lost ein

Die Schlüsselfehler lohnen eine eigene Lektüre, denn dieselbe Antwort deckt einen fehlenden und einen schlicht falschen Schlüssel ab: so beheben Sie ERROR_WRONG_USER_KEY.

FAQ

Sollten Lösen und Absenden zwei getrennte Tasks sein?

Nein. Die Aufteilung ist verlockend, weil die beiden Hälften aus verschiedenen Gründen scheitern und eine Chain im Monitor aufgeräumter aussieht. Aber ein reCAPTCHA-Token hält etwa zwei Minuten, und ein Task in der Queue kann länger liegen bleiben, die zweite Hälfte läuft also regelmäßig gegen einen Token, der bereits abgelaufen ist. Lassen Sie beides zusammen und lassen Sie das Ganze als Einheit wiederholen. Erneutes Lösen kostet Sie ein paar Sekunden Ihres eigenen Rechners.

Ist acks_late bei einem Captcha-Lösevorgang sicher?

Ja, solange der Task neu löst statt fortzusetzen. Späte Bestätigung heißt, dass der Job eines abgestürzten Workers erneut zugestellt wird und ein zweites Mal läuft, der Task muss also gefahrlos wiederholbar sein. Ein Task, der jedes Mal eine frische Challenge absendet, ist das. Ein Task, der eine id gespeichert hat und sie erneut zu lesen versucht, ist es nicht, denn ein Ergebnis kann nur einmal gelesen werden. Die Variante in dieser Anleitung ist die sichere Form.

Können die Worker unter Linux laufen, wenn der Solver unter Windows läuft?

Ja, und das ist die normale Anordnung. Der Worker muss nur einen HTTP-Endpunkt erreichen, er kann also ein Linux-Container irgendwo im Netzwerk sein, während der Solver auf einem Windows-Rechner im Server-Modus läuft. Richten Sie CAPSKIP_HOST auf diesen Rechner. Am Lösevorgang wird nichts abgerechnet und nichts wird fremd in dem Sinne, auf den es ankommt: Die Hardware gehört weiterhin Ihnen.

Worin unterscheidet sich das vom Lösen in Airflow?

Airflow plant einen Graphen aus Schritten und reicht Daten zwischen ihnen weiter, die interessante Frage dort ist also, welche Grenze der Token überquert. Celery ist eine Queue, die interessante Frage ist also, was passiert, wenn dieselbe Nachricht zweimal zugestellt wird. Der Löse-Aufruf ist identisch. Die Frage nach der Grenze wird vollständig durchgearbeitet in dem Airflow-DAG-Leitfaden.

Die Kurzfassung

Packen Sie den Lösevorgang und alles, was den Token verwendet, in einen Task. Setzen Sie ein Soft Limit von 330 und ein hartes Limit von 360, damit das Timeout des Clients zuerst meldet. Wiederholen Sie nur bei NetworkException, und lassen Sie den Retry neu lösen statt fortsetzen, weil ein Backoff einen Token bei Weitem überdauern kann und ein Ergebnis nur einmal gelesen werden kann. Schalten Sie die späte Bestätigung ein, senken Sie den Prefetch-Multiplikator auf 1 und halten Sie das Visibility Timeout von Redis weit über dem harten Limit. Betreiben Sie CapSkip im Server-Modus, sobald die Worker nicht auf dem Rechner des Solvers selbst laufen.

Eines sollten Sie noch abwägen, bevor Sie die Queue dimensionieren: CapSkip ist ein Captcha-Löser und läuft auf Hardware, die Sie bereits besitzen, sodass hundert Worker, die darauf einhämmern, und ein einzelner Worker, der dieselben Jobs langsam abarbeitet, gleich viel kosten, nämlich nichts.