Captchas in httpx und aiohttp lösen (ohne Browser)

httpx captcha - How to Solve CAPTCHAs in httpx and aiohttp (No Browser)

Ein httpx-Captcha-Ablauf besteht aus drei Schritten und rund zwanzig Zeilen Code, und der Grund, warum ihn viele falsch bauen, hat nichts mit dem Löser zu tun. In diesem Stack gibt es keinen Browser. Nichts führt das JavaScript der Website aus, es entsteht also kein verstecktes Feld, in dem das Token landen soll, und nichts sendet das Formular für Sie ab. Alle drei erledigen Sie selbst: Sie lesen den Sitekey aus dem HTML, lösen das Captcha und schicken das Token anschließend als ganz normales Formularfeld über denselben Client zurück, der die Seite abgerufen hat.

Was Sie brauchen

  • Python 3.10 oder neuer und entweder httpx oder aiohttp. Das CapSkip Python-SDK funktioniert mit beiden.
  • Die URL der geschützten Seite. Den Sitekey brauchen Sie nicht im Voraus, denn Schritt eins liest ihn 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.
# pip install capskip
pip install capskip httpx

# Using aiohttp instead? Install it as well. See the note below
# about which HTTP libraries you end up with either way.
pip install capskip aiohttp

Das SDK bringt httpx bereits mit

Gut zu wissen, bevor Sie Abhängigkeiten zählen. Das CapSkip Python-Paket deklariert requests, httpx und aiofiles als harte Laufzeitabhängigkeiten, mit dem SDK wird httpx also mitinstalliert, ob Sie es angefordert haben oder nicht. Wenn httpx ohnehin Ihr Scraping-Client ist, fügt der Löser Ihrer Umgebung keine neue HTTP-Bibliothek hinzu, und AsyncCapSkip teilt sich Ihre Event Loop. Wenn Sie auf aiohttp setzen, kommt httpx zusätzlich dazu, und Sie haben nun zwei Clients im selben virtualenv. Kaputt geht dabei nichts, aber es ist die Art von Detail, nach der ein Dependency-Review fragen wird.

Was sich ändert, wenn es keinen Browser gibt

In Selenium oder Playwright setzen Sie den Wert der versteckten Textarea, die reCAPTCHA rendert, und der seiteneigene Submit-Handler trägt ihn mit. Nichts davon existiert hier. Ein HTTP-Client holt Bytes ab, und niemand wertet die Script-Tags aus, das Widget wird also nie gerendert, die Textarea nie erzeugt, und es gibt auch keinen Submit-Handler, der laufen könnte.

Was Sie stattdessen haben, ist einfacher und leichter zu durchschauen. Das Token ist nur eine Zeichenkette, und die Website erwartet sie in einem POST-Body unter einem Feldnamen. Bei reCAPTCHA v2 heißt dieses Feld g-recaptcha-response, und der Name ist derselbe, ob nun der Browser es gefüllt hat oder Sie. Turnstile verwendet cf-turnstile-response. GeeTest sendet drei Felder statt einem. Die Verifizierung läuft auf dem Server der Website gegen Google oder Cloudflare, niemand am anderen Ende kann also erkennen, welcher Ihrer Prozesse die Zeichenkette erzeugt hat.

Schritt 1: Lesen Sie den Sitekey mit dem Client, mit dem Sie später absenden

Verwenden Sie einen einzigen Client für den gesamten Ablauf. Das ist keine Ordnungsliebe. Das ist der Kern der Sache: Cookie-Jar und Connection Pool sorgen dafür, dass der POST so aussieht, als käme er vom selben Besucher wie der GET. Bauen Sie für das Absenden einen frischen Client, werfen Sie jedes Session-Cookie weg, das die Seite gesetzt hat, und das ist ein sehr häufiger Grund dafür, dass ein korrektes Token trotzdem scheitert.

# pip install capskip httpx
import re
import httpx

PAGE_URL = "https://example.com/page-with-recaptcha"

# One client for the whole flow. Its cookie jar is what makes
# the POST later look like the same visitor as this GET.
client = httpx.Client(timeout=30, follow_redirects=True)

html = client.get(PAGE_URL).text
match = re.search(r'data-sitekey=["\']([^"\']+)', html)
if not match:
    raise RuntimeError("No data-sitekey in the HTML.")

sitekey = match.group(1)   # this is what the solver needs

Wenn diese Regex nichts findet, wird das Widget per JavaScript eingefügt statt im Markup ausgeliefert, und ein HTTP-Client bekommt es nie zu sehen. Öffnen Sie die Seite einmal im Browser, holen Sie den Sitekey aus dem Netzwerk-Tab und schreiben Sie ihn fest in den Code. Ein Sitekey ist öffentlich und stabil, das ist also keine Abkürzung, für die Sie sich schämen müssten.

Schritt 2: Lösen Sie es auf Ihrer eigenen Maschine

Das Lösen ist ein einziger Aufruf gegen einen Dienst, der auf Ihrer eigenen Hardware lauscht. Jede reCAPTCHA-Variante ist dieselbe Methode mit anderen Keyword-Argumenten: invisible auf 1, enterprise auf 1, oder version auf v3 mit einer action. Turnstile und GeeTest haben eigene Methoden mit demselben Aufbau. Die vollständige Parameterliste finden Sie in der CapSkip-API-Dokumentation.

# CapSkip listens on your machine, so this is a loopback call.
from capskip import CapSkip

solver = CapSkip(host="127.0.0.1", port=8080)
token = solver.recaptcha(sitekey=sitekey, url=PAGE_URL)["code"]

Dieser Aufruf blockiert, solange er pollt. Das SDK pollt nicht in einem festen Intervall: Es startet bei 250 Millisekunden und nähert sich schrittweise pollingInterval an, weshalb eine Lösung über das SDK meist früher zurückkommt als eine von Hand geschriebene Schleife gegen den rohen Endpunkt. Die Obergrenze ist recaptchaTimeout, standardmäßig 300 Sekunden.

Schritt 3: Senden Sie das Token als Formularfeld

Schicken Sie es jetzt über denselben Client zurück, in demselben Formular, das auch der Browser abgeschickt hätte. Nehmen Sie die übrigen Felder des Formulars mit, einschließlich jedes versteckten CSRF- oder Nonce-Werts, den Sie in Schritt eins aus dem HTML gelesen haben.

# g-recaptcha-response is an ordinary form field. Same client,
# so the session cookies from the GET go along with it.
reply = client.post(
    PAGE_URL,
    data={
        "username": "demo",
        "g-recaptcha-response": token,
    },
)

print(reply.status_code)

Senden Sie das Token sofort. Ein reCAPTCHA-Token ist rund zwei Minuten gültig, und eine Warteschlange, ein Retry-Backoff oder ein sleep zwischen Lösen und Absenden verbrennt dieses Budget, ohne dass Sie es merken. Diese Lebensdauer und was sie in der Praxis bedeutet, behandeln wir in dem Leitfaden zum Ablaufen von reCAPTCHA-Tokens.

Vollständiges funktionierendes Beispiel, async

Dieselben drei Schritte mit dem asynchronen Client. AsyncCapSkip ist in Python eine echte async-Implementierung und kein Alias für die synchrone Variante, es teilt sich also Ihre Event Loop und parkt keinen Thread, während es pollt.

# pip install capskip httpx
import asyncio
import re
import httpx
from capskip import AsyncCapSkip

PAGE_URL = "https://example.com/page-with-recaptcha"
SITEKEY_RE = re.compile(r'data-sitekey=["\']([^"\']+)')

async def submit_once(client, solver):
    html = (await client.get(PAGE_URL)).text
    match = SITEKEY_RE.search(html)
    if not match:
        raise RuntimeError("No data-sitekey in the HTML.")

    result = await solver.recaptcha(sitekey=match.group(1), url=PAGE_URL)

    reply = await client.post(
        PAGE_URL,
        data={"g-recaptcha-response": result["code"]},
    )
    return reply.status_code

async def main():
    solver = AsyncCapSkip(host="127.0.0.1", port=8080)
    async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
        print(await submit_once(client, solver))

asyncio.run(main())

Um mehrere davon gleichzeitig laufen zu lassen, sammeln Sie sie mit gather ein. Eine Löser-Instanz reicht für den ganzen Batch, und ein Client pro Identität ist meist das, was Sie wollen, denn ein geteiltes Cookie-Jar bedeutet eine geteilte Session. Die Batching-Seite davon ist beschrieben in der Anleitung zum parallelen Lösen von Captchas mit Python.

Die aiohttp-Variante

Alles oben Gesagte gilt weiter. Nur drei Namen ändern sich: der Session-Typ, die Art, wie Sie einen Body auslesen, und das Attribut, das den Statuscode enthält.

# pip install capskip aiohttp
import aiohttp
from capskip import AsyncCapSkip

async def submit_once(session, solver):
    async with session.get(PAGE_URL) as reply:
        html = await reply.text()

    match = SITEKEY_RE.search(html)
    result = await solver.recaptcha(sitekey=match.group(1), url=PAGE_URL)

    async with session.post(
        PAGE_URL,
        data={"g-recaptcha-response": result["code"]},
    ) as submitted:
        return submitted.status   # not status_code

async def main():
    solver = AsyncCapSkip(host="127.0.0.1", port=8080)
    async with aiohttp.ClientSession() as session:
        print(await submit_once(session, solver))

Eine ClientSession führt ihr eigenes Cookie-Jar, die Ein-Client-Regel gilt hier also unverändert. aiohttp unterstützt kein HTTP/2, was Sie für diese Aufgabe nichts kostet.

Proxys und die Falle, die nur HTTP-Clients erwischt

Wenn die Website prüft, ob das Token von der Adresse erzeugt wurde, die es auch absendet, müssen Lösung und Absenden über denselben Proxy hinausgehen. CapSkip nimmt für reCAPTCHA, Turnstile und GeeTest einen Proxy pro Aufgabe entgegen. Bild-Captchas akzeptieren keinen und brauchen auch keinen.

# Same exit address for the solve and for the submit.
result = await solver.recaptcha(
    sitekey=sitekey,
    url=PAGE_URL,
    proxy={"type": "HTTP", "uri": "user:[email protected]:3128"},
)

# httpx 0.26 and newer spell this proxy=. Before that it was
# proxies=, which 0.28 removed outright.
async with httpx.AsyncClient(proxy="http://user:[email protected]:3128") as client:
    await client.post(PAGE_URL, data={"g-recaptcha-response": result["code"]})

Und hier ist der Teil, der viele erwischt. CapSkip akzeptiert SOCKS5 und SOCKS5H als Proxy-Typen, aber aiohttp spricht nur HTTP-Proxys sowie HTTPS über einen CONNECT-Tunnel, und httpx braucht das socks-Extra installiert, bevor es überhaupt SOCKS beherrscht. Geben Sie dem Löser einen SOCKS5-Proxy, den Ihr Client nicht nutzen kann, dann gelingt die Lösung von der einen Adresse, während das Absenden von einer anderen ausgeht, und das sieht exakt aus wie ein schlechtes Token, ist aber keines. Halten Sie beide Enden auf einem Proxy-Typ, den beide sprechen können. Die Wahl zwischen den Proxy-Typen behandeln wir in dem Leitfaden zu rotierenden Proxys beim Lösen.

Noch eine aiohttp-Besonderheit: Anders als requests ignoriert es die Umgebungsvariablen HTTP_PROXY und HTTPS_PROXY, sofern Sie die Session nicht mit trust_env auf True erzeugen. Ein Proxy, den Sie für gesetzt hielten, war es schlicht nicht, und nichts hat Ihnen das gesagt.

HTTP/2 und was sich ändert, wenn Sie es einschalten

httpx kann HTTP/2 sprechen, aber nicht standardmäßig und nicht ohne das Extra: Installieren Sie httpx mit dem http2-Extra und übergeben Sie beim Aufbau des Clients http2 auf True. Wichtig zu wissen ist, dass sich dadurch ändert, wie Ihr Traffic auf der Leitung aussieht, denn Protokollaushandlung und Header-Reihenfolge unterscheiden sich von HTTP/1.1. Das ist eher eine Frage des Fingerprintings als des Lösens und ein eigenes Thema, kurz umrissen in dem Artikel zu TLS-Fingerprinting in Python.

Den Löser auf einer anderen Maschine betreiben

Skripte wandern. Ein Container, ein VPS oder ein geplanter Worker ist nicht die Maschine, auf der der Löser läuft, und Loopback zeigt dort auf den Container, wo nichts lauscht.

CapSkip hat zwei Verbindungsmodi. Der Lokal-Modus bindet an 127.0.0.1 und antwortet nur diesem Gerät. Der Server-Modus bindet an Ihre Netzwerkadresse oder öffentliche IP, sodass ein Container, eine VM oder ein gehosteter Worker dieselbe Windows-Maschine über die API erreicht. Eine statische öffentliche IP hält die Adresse stabil. Es bleibt so oder so Ihre Hardware und bleibt ohne Verbrauchsabrechnung, ein arbeitsreicher Tag kostet also in beiden Modi dasselbe.

# The SDK reads these three itself, so the same code works
# whether the solver is local or on another machine:
#   CAPSKIP_HOST=192.0.2.10
#   CAPSKIP_PORT=8080
#   CAPSKIP_API_KEY=your-key

solver = AsyncCapSkip()

Schalten Sie die Schlüsselvalidierung ein, sobald der Löser auf einer Netzwerkadresse lauscht, und geben Sie jedem Worker einen eigenen Schlüssel, damit einer widerrufen werden kann, ohne die anderen anzufassen. Beide Modi werden durchgegangen in der CapSkip-Einrichtungsanleitung.

Häufige Fehler und was sie bedeuten

Was Sie sehenUrsacheBeheben
Die Regex findet kein data-sitekeyDas Widget wird per JavaScript eingefügt und steht daher nicht im ausgelieferten HTMLSitekey einmal im Browser auslesen und fest im Code hinterlegen. Er ist öffentlich und stabil
Das Formular kommt erneut mit der Challenge zurückFür den POST wurde ein zweiter Client gebaut, die Session-Cookies gingen dadurch verlorenEinen einzigen Client für GET und POST verwenden
Ein gültiges Token wird abgelehntLösung und Absenden gingen von unterschiedlichen Adressen ausAuf beiden Seiten denselben Proxy nutzen, und einen Typ, den beide Enden sprechen
Ein gültiges Token wird nach einer Verzögerung abgelehntEs ist in einer Warteschlange zwischen Lösung und Absenden abgelaufenSo spät wie möglich lösen und sofort absenden
Der Proxy scheint unter aiohttp ignoriert zu werdenaiohttp liest Proxy-Umgebungsvariablen standardmäßig nichtproxy= explizit übergeben, oder die Session mit trust_env auf True bauen
TypeError im Konstruktor des httpx-Clientsproxies= wurde in httpx 0.28 entferntproxy= verwenden, so heißt der Parameter seit 0.26
NetworkExceptionCapSkip läuft nicht, oder Host und Port sind falschDie App starten, oder CAPSKIP_HOST auf die Serveradresse richten
ValidationExceptionEin Argument, das das SDK für diesen Captcha-Typ nicht akzeptiertDen Typ prüfen. action bei v2 oder invisible bei v3 löst diesen Fehler aus

FAQ

Funktioniert reCAPTCHA v2 wirklich ohne Browser?

Ja. Der Server der Website verifiziert das Token bei Google, und diese Prüfung betrachtet das Token, den Sitekey und die Adresse, von der es kam. Diese Prüfung hat keine Möglichkeit zu erfragen, welcher Prozess die Zeichenkette erzeugt hat. Der Browser war immer nur ein bequemer Ort, um das Feld unterzubringen.

Zwingt mir die Installation des SDK httpx auf?

Ja. requests, httpx und aiofiles sind deklarierte Abhängigkeiten des Pakets, httpx kommt also selbst in einem aiohttp-Projekt mit. Das ist ein zweiter HTTP-Client in der Umgebung und kein Konflikt, und nichts in Ihrem eigenen Code muss ihn verwenden.

Wie mache ich das für reCAPTCHA v3?

Übergeben Sie version auf v3 und die action, die die Seite verwendet, und senden Sie das Token dann unter dem Feldnamen, in den das seiteneigene Skript es schreibt. Dieser Name ist nicht so standardisiert wie g-recaptcha-response, lesen Sie ihn also einmal aus der Seite aus. Die action muss übereinstimmen, sonst scheitert die Prüfung auf der Serverseite, obwohl das Token echt ist.

Sollte ich dafür httpx oder aiohttp verwenden?

Beides geht. httpx ist mit dem SDK ohnehin installiert und beherrscht mit den passenden Extras HTTP/2 und SOCKS, womit es die Wahl ist, die Ihrer Umgebung nichts Neues hinzufügt. aiohttp ist bei sehr hoher Nebenläufigkeit schneller und ist die Grundlage vieler bestehender Crawler. Der Löser-Code ist für beide identisch, entscheiden Sie also nach den Vorzügen des Crawlers und nicht nach der Captcha-Seite.

Die Kurzfassung

Lesen Sie den Sitekey aus dem ausgelieferten HTML, lösen Sie das Captcha lokal, und senden Sie das Token dann als ganz normales Formularfeld über den Client, der die Seite abgerufen hat. Ein Client für den gesamten Ablauf, damit die Cookies überleben. Dieselbe Ausgangsadresse für Lösung und Absenden, auf einem Proxy-Typ, den beide Enden tatsächlich sprechen. Senden Sie sofort ab, denn das Token ist kurzlebig.

Den weiteren Python-Kontext finden Sie auf der Python-Captcha-Solver-Seite. Die Details zu reCAPTCHA v2 finden Sie auf die reCAPTCHA-v2-Solver-Seite. Dieselben drei Aufrufe gibt es auch in Node.js, PHP und C#. Aufgeführt sind sie hier: Die Seite zu den SDKs fürs Captcha-Lösen.

Eine letzte Sache, die Sie wissen sollten, bevor Sie das auf einen großen Crawl loslassen. CapSkip ist ein unbegrenzter Captcha-Löser der auf Hardware läuft, die Ihnen bereits gehört, sodass ein Crawl, der fünfzigtausend Challenges löst, genau so viel kostet wie einer, der fünfzig löst.