Der Engpass beim automatisierten CAPTCHA-Lösen ist selten die Rechenzeit, sondern das Warten. Zwischen der Übermittlung an in.php und dem fertigen Token vergehen je nach Typ wenige Sekunden bis knapp eine Minute – Zeit, in der ein synchrones Skript blockiert. Mit aiohttp warten zwanzig Aufgaben nebeneinander statt nacheinander, und die Gesamtlaufzeit richtet sich nach der langsamsten Lösung, nicht nach ihrer Summe.
Dieser Leitfaden zeigt einen vollständigen asynchronen CaptchaAI-Client: übermitteln, Status abfragen, Token weiterverwenden. Dazu kommt, was im Dauerbetrieb zählt – Parallelität sinnvoll begrenzen, Ausnahmen einzelner Aufgaben abfangen und denselben Client für reCAPTCHA v2 wie für Cloudflare Turnstile nutzen.
Warum asynchron: Das Polling bestimmt die Laufzeit
Der Ablauf besteht immer aus zwei Anfragen und einer Wartezeit dazwischen. Sie übermitteln Sitekey und Page-URL an in.php und erhalten eine Task-ID; danach fragen Sie res.php im Sekundentakt ab, bis das Token vorliegt. reCAPTCHA v2 wird typischerweise in unter 60 Sekunden gelöst, Cloudflare Turnstile in unter 10 Sekunden – der Prozess selbst ist in dieser Zeit vollständig untätig.
Genau dieses Warten lässt sich überlappen. asyncio gibt die Kontrolle während await an den Event-Loop zurück, sodass ein einzelner Python-Prozess dutzende Lösungen gleichzeitig verfolgt, ohne Threads oder zusätzliche Worker-Prozesse. aiohttp liefert dazu den passenden HTTP-Client mit Verbindungspooling. Wer denselben Ablauf lieber synchron und asynchron aus einer Codebasis fahren möchte, findet die Alternative im Leitfaden zur HTTPX-Integration.
Voraussetzungen
| Anforderung | Details |
|---|---|
| Python | 3.8+ |
| aiohttp | 3.8+ |
| CaptchaAI-API-Schlüssel | Konto anlegen und Schlüssel kopieren |
pip install aiohttp
Der Schlüssel gehört nicht in den Quellcode, sondern in die Umgebungsvariable CAPTCHAAI_API_KEY – alle folgenden Beispiele lesen ihn von dort.
Der asynchrone CaptchaAI-Client
Die Klasse kapselt den kompletten Ablauf: submit() übermittelt die Aufgabe und liefert die Task-ID, poll() fragt das Ergebnis ab, solve() verbindet beides, get_balance() liest das Guthaben.
import aiohttp
import asyncio
class AsyncCaptchaAI:
def __init__(self, api_key):
self.api_key = api_key
self.base_url = "https://ocr.captchaai.com"
async def submit(self, session, params):
"""Submit a CAPTCHA task and return the task ID."""
params["key"] = self.api_key
async with session.get(
f"{self.base_url}/in.php", params=params
) as resp:
text = await resp.text()
if not text.startswith("OK|"):
raise Exception(f"Submit failed: {text}")
return text.split("|")[1]
async def poll(self, session, task_id, timeout=300):
"""Poll for the result with a timeout."""
params = {
"key": self.api_key,
"action": "get",
"id": task_id,
}
deadline = asyncio.get_event_loop().time() + timeout
while asyncio.get_event_loop().time() < deadline:
await asyncio.sleep(5)
async with session.get(
f"{self.base_url}/res.php", params=params
) as resp:
text = await resp.text()
if text == "CAPCHA_NOT_READY":
continue
if text.startswith("OK|"):
return text.split("|", 1)[1]
raise Exception(f"Solve failed: {text}")
raise TimeoutError(f"Task {task_id} timed out after {timeout}s")
async def solve(self, session, params, timeout=300):
"""Submit and poll in one call."""
task_id = await self.submit(session, params)
return await self.poll(session, task_id, timeout)
async def get_balance(self, session):
"""Check account balance."""
params = {"key": self.api_key, "action": "getbalance"}
async with session.get(
f"{self.base_url}/res.php", params=params
) as resp:
return float(await resp.text())
Zwei Details sind wichtiger, als sie aussehen. CAPCHA_NOT_READY ist kein Fehler, sondern der erwartete Zustand in den ersten Sekunden – nur eine Antwort, die weder so lautet noch mit OK| beginnt, rechtfertigt einen Abbruch. Und die Session wird bewusst von außen übergeben: Ein einziges ClientSession-Objekt bedient alle Aufgaben und hält den Verbindungspool warm.
Ein einzelnes CAPTCHA lösen
Der Einstieg besteht aus einem Guthaben-Check als kurzem Health-Check und einer reCAPTCHA-v2-Aufgabe mit method, googlekey und pageurl.
import asyncio
import os
async def main():
solver = AsyncCaptchaAI(os.environ["CAPTCHAAI_API_KEY"])
async with aiohttp.ClientSession() as session:
# Check balance
balance = await solver.get_balance(session)
print(f"Balance: ${balance:.2f}")
# Solve reCAPTCHA v2
token = await solver.solve(session, {
"method": "userrecaptcha",
"googlekey": "6Le-wvkS...",
"pageurl": "https://example.com",
})
print(f"Token: {token[:50]}...")
asyncio.run(main())
Das zurückgegebene Token tragen Sie anschließend als g-recaptcha-response in das Formular ein und senden es ab. Lösen und Absenden gehören in denselben Task: reCAPTCHA-Tokens laufen nach rund zwei Minuten ab.
Mehrere CAPTCHAs gleichzeitig lösen
asyncio.gather() startet alle Aufgaben zusammen und liefert die Ergebnisse in der Reihenfolge der Eingabeliste zurück.
async def solve_batch(urls, site_key):
solver = AsyncCaptchaAI(os.environ["CAPTCHAAI_API_KEY"])
async with aiohttp.ClientSession() as session:
tasks = [
solver.solve(session, {
"method": "userrecaptcha",
"googlekey": site_key,
"pageurl": url,
})
for url in urls
]
results = await asyncio.gather(*tasks, return_exceptions=True)
for url, result in zip(urls, results):
if isinstance(result, Exception):
print(f"FAILED {url}: {result}")
else:
print(f"SOLVED {url}: {len(result)} chars")
return results
urls = [
"https://example.com/page1",
"https://example.com/page2",
"https://example.com/page3",
"https://example.com/page4",
"https://example.com/page5",
]
asyncio.run(solve_batch(urls, "6Le-wvkS..."))
return_exceptions=True ist hier die halbe Fehlerbehandlung: Ohne diesen Parameter reißt eine einzige gescheiterte Aufgabe den gesamten gather()-Aufruf ab. So bleibt der Rest des Stapels intakt, und Sie protokollieren pro URL, was tatsächlich passiert ist.
Nur lösen, wenn die Seite ein CAPTCHA zeigt
In der Praxis liefert nicht jede Seite eine CAPTCHA-Abfrage aus. Der folgende Ablauf holt zuerst das HTML, prüft auf den reCAPTCHA-Marker und ruft den Solver nur im Bedarfsfall auf – das spart Threads und Laufzeit.
async def scrape_with_captcha(url, site_key):
solver = AsyncCaptchaAI(os.environ["CAPTCHAAI_API_KEY"])
async with aiohttp.ClientSession() as session:
# Fetch the page
async with session.get(url) as resp:
html = await resp.text()
# Check if page has a CAPTCHA
if "g-recaptcha" not in html:
return html # No CAPTCHA, return content
# Solve the CAPTCHA
token = await solver.solve(session, {
"method": "userrecaptcha",
"googlekey": site_key,
"pageurl": url,
})
# Submit with solved token
async with session.post(url, data={
"g-recaptcha-response": token,
}) as resp:
return await resp.text()
Für den produktiven Einsatz kommen eine Wiederholungslogik mit exponentiellem Backoff für Netzwerkfehler und eine saubere Auswertung der res.php-Fehlercodes dazu: Ein Konto-Problem behandeln Sie anders als eine Zeitüberschreitung.
Parallelität begrenzen: Semaphore und Thread-Kontingent
Wie viele Lösungen wirklich gleichzeitig laufen, entscheidet nicht asyncio, sondern Ihr Tarif. CaptchaAI rechnet Thread-basiert ab – ein Thread ist ein CAPTCHA in Bearbeitung, die Zahl der Lösungen je Thread ist im Abrechnungsmonat nicht gedeckelt. BASIC (15 $/Monat) stellt 5 Threads bereit, STANDARD (30 $/Monat) 15, ADVANCE (90 $/Monat) 50 und PREMIUM (170 $/Monat) 100. Setzen Sie max_concurrent auf Ihre Thread-Zahl, statt eine offene Liste in gather() zu werfen.
async def solve_with_limit(urls, site_key, max_concurrent=10):
solver = AsyncCaptchaAI(os.environ["CAPTCHAAI_API_KEY"])
semaphore = asyncio.Semaphore(max_concurrent)
async def solve_one(session, url):
async with semaphore:
return await solver.solve(session, {
"method": "userrecaptcha",
"googlekey": site_key,
"pageurl": url,
})
async with aiohttp.ClientSession() as session:
tasks = [solve_one(session, url) for url in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
solved = sum(1 for r in results if not isinstance(r, Exception))
print(f"Solved {solved}/{len(urls)} CAPTCHAs")
return results
Die Semaphore hält die Warteschlange im Prozess, statt sie an die API weiterzureichen. Für eine Liste mit 500 URLs und 15 Threads heißt das: 15 Lösungen laufen, der Rest wartet geordnet.
Turnstile lösen: Nur die Payload ändert sich
Der Client bleibt unverändert, Sie tauschen lediglich die Parameter aus. Statt googlekey erwartet Turnstile den sitekey.
async def solve_turnstile(url, sitekey):
solver = AsyncCaptchaAI(os.environ["CAPTCHAAI_API_KEY"])
async with aiohttp.ClientSession() as session:
token = await solver.solve(session, {
"method": "turnstile",
"sitekey": sitekey,
"pageurl": url,
})
return token
Nach demselben Muster laufen weitere Typen über dieselbe solve()-Methode:
| CAPTCHA-Typ | Wert für method |
|---|---|
| reCAPTCHA v2 / v3 (inkl. Invisible, Enterprise) | userrecaptcha |
| Cloudflare Turnstile | turnstile |
| Cloudflare Challenge | cloudflare_challenge |
| GeeTest v3 | geetest |
| Bild- und Rasterbild-CAPTCHA | post |
| BLS | bls |
Nicht unterstützt sind hCaptcha und FunCaptcha; GeeTest v4 ist bislang lediglich als bald verfügbar angekündigt. CaptchaFox (Beta), Friendly Captcha (Beta) und Lemin (Beta) sind über eigene method-Werte erreichbar.
Praxisbeispiel: nächtlicher Aggregationslauf
Eine Hamburger Agentur zieht jede Nacht öffentlich zugängliche Angebotsseiten von rund 15 Partnerportalen zusammen. Etwa jedes dritte Portal antwortet mit einer Turnstile-Abfrage. Der Job läuft als Container auf einem kleinen netcup-Server, aiohttp verarbeitet die Portale parallel, eine Semaphore mit 15 Slots deckt sich exakt mit dem Thread-Kontingent. Da Turnstile typischerweise in unter 10 Sekunden gelöst wird, endet der komplette Lauf in wenigen Minuten – ohne Headless-Browser, also mit deutlich geringerem Speicherbedarf auf der Instanz.
Zwei Punkte gehören im deutschsprachigen Umfeld in die Planung: IP-Adressen gelten nach DSGVO als personenbezogene Daten, sodass Protokollierung und Aufbewahrung Ihrer Abrufdaten dokumentiert sein sollten – und Rechnungsbeträge fallen in US-Dollar an, der Euro-Gegenwert schwankt mit dem Wechselkurs.
Fehler richtig einordnen
| Fehler | Ursache | Lösung |
|---|---|---|
ClientConnectorError |
Netzwerkproblem | Konnektivität und DNS prüfen |
Submit failed: ERROR_ZERO_BALANCE |
Guthaben aufgebraucht | Konto aufladen |
Submit failed: ERROR_WRONG_USER_KEY |
Schlüssel falsch übergeben | Umgebungsvariable prüfen |
TimeoutError |
Lösung dauert länger als das Limit | timeout-Parameter erhöhen |
RuntimeError: Event loop is closed |
asyncio.run in Jupyter |
nest_asyncio verwenden |
FAQ
Wie viele Lösungen darf ich gleichzeitig starten?
So viele, wie Ihr Tarif an Threads bereitstellt: 5 bei BASIC, 15 bei STANDARD, 50 bei ADVANCE. Die Zahl der Lösungen je Thread ist im Monat nicht begrenzt – begrenzt ist nur, wie viele davon zeitgleich laufen. Eine asyncio.Semaphore in Höhe dieser Zahl hält gather() verlässlich in diesem Rahmen.
Was passiert, wenn eine einzelne Aufgabe im gather() fehlschlägt?
Mit return_exceptions=True landet die Ausnahme als Ergebnis in der Liste, alle übrigen Aufgaben laufen weiter. Ohne den Parameter bricht der gesamte Aufruf beim ersten Fehler ab. Prüfen Sie jedes Ergebnis mit isinstance(result, Exception) und wiederholen Sie gezielt nur die betroffenen URLs.
Blockiert await asyncio.sleep(5) beim Abfragen die anderen Aufgaben?
Nein. asyncio.sleep() gibt die Kontrolle an den Event-Loop zurück, sodass in dieser Zeit andere Aufgaben ihre HTTP-Anfragen abarbeiten. Blockierend wäre nur time.sleep() – dieser Aufruf hat in asynchronem Code nichts zu suchen.
Brauche ich zusätzlich einen Headless-Browser?
Nein. Der Client spricht ausschließlich HTTP: Sitekey und Page-URL übermitteln, Token abholen, Formular absenden. Ein Browser wird erst nötig, wenn die Zielanwendung ihre Formulardaten selbst per JavaScript zusammenbaut.
Warum meldet der Code in Jupyter Event loop is closed?
Weil Notebooks bereits einen Event-Loop betreiben und asyncio.run() einen zweiten anlegen will. Rufen Sie in Notebooks die Coroutine direkt mit await auf oder installieren Sie nest_asyncio.