Crawlee bringt Session-Pool, Proxy-Rotation, Request-Queue und Wiederholungslogik von Haus aus mit – einen CAPTCHA-Solver jedoch nicht. Diese Lücke schließen Sie mit einer einzigen async-Funktion: Sitekey aus dem DOM lesen, die Aufgabe an die HTTP-API von CaptchaAI übermitteln, das Ergebnis abfragen und das Token im requestHandler in das Formular eintragen. Der Rest Ihres Crawlers bleibt unverändert.
Dieser Leitfaden zeigt dasselbe Muster dreimal: für den CheerioCrawler bei statischem HTML, für den PlaywrightCrawler bei JavaScript-gerenderten Seiten und für den Session-Pool bei längeren Läufen. Dazu kommt, was in der Praxis meist fehlt: Thread-Planung, Fehlercodes und ein Beispiel aus einem DACH-Setup.
Was Crawlee abdeckt – und was Sie ergänzen
| Crawlee-Baustein | Beitrag zur CAPTCHA-Lösung |
|---|---|
| Session-Pool | Cookies und Browser-Signale bleiben pro Sitzung konsistent, ein gelöstes CAPTCHA verliert seinen Kontext nicht |
| Automatische Wiederholung | fehlgeschlagene Anfragen laufen nach der Lösung erneut durch die Queue |
| Proxy-Rotation | verteilt die Anfragen; die Lösung selbst läuft unabhängig davon über die API |
| Request-Queue | Seiten mit und ohne CAPTCHA lassen sich im selben Durchlauf abarbeiten |
Ein CAPTCHA erkennt Crawlee nicht von allein. Sie prüfen im requestHandler, ob ein Element mit data-sitekey oder ein bekannter Container im DOM liegt, und verzweigen erst dann in die Lösungsroutine. Zeitplanung, Nebenläufigkeit und Fehlerbehandlung bleiben Sache des Frameworks.
Vorbereitung: API-Schlüssel und Threads für den Crawlee-Lauf
Legen Sie den Schlüssel als Umgebungsvariable CAPTCHAAI_API_KEY ab und stimmen Sie maxConcurrency auf Ihr Thread-Kontingent ab. CaptchaAI rechnet pro gleichzeitigem Thread ab – nicht pro Lösung –, und jeder Plan enthält unbegrenzt viele Lösungen pro Thread im Abrechnungsmonat. Ein Thread ist ein CAPTCHA, das gerade in Bearbeitung ist; sobald es gelöst ist, nimmt derselbe Thread die nächste Aufgabe an.
- BASIC (15 $/Monat, 5 Threads): passt zu
maxConcurrency: 5aus dem ersten Beispiel. - STANDARD (30 $/Monat, 15 Threads): mehrere Crawler parallel oder ein Playwright-Lauf mit Reserve.
- ADVANCE (90 $/Monat, 50 Threads): Dauerbetrieb mit mehreren Apify-Actors und Nachtläufen.
Preise in US-Dollar. Für die Planung zählt nicht die Zahl der URLs in der Queue, sondern wie viele Lösungen gleichzeitig offen sind.
CheerioCrawler: Token lösen, Formular absenden
Der CheerioCrawler lädt reines HTML, ohne Browser – schnell und ressourcenschonend, dafür senden Sie das Formular selbst ab. Der Ablauf: Sitekey auslesen, Aufgabe an in.php übermitteln, 15 Sekunden warten und danach res.php im 5-Sekunden-Takt abfragen, bis ein Token zurückkommt. Dieses Token übergeben Sie anschließend als Feld g-recaptcha-response in der POST-Anfrage.
const { CheerioCrawler } = require('crawlee');
const https = require('https');
const API_KEY = process.env.CAPTCHAAI_API_KEY;
async function solveCaptcha(sitekey, pageurl) {
// Submit task
const submitData = new URLSearchParams({
key: API_KEY,
method: 'userrecaptcha',
googlekey: sitekey,
pageurl: pageurl,
json: '1',
});
const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
method: 'POST',
body: submitData,
});
const submitResult = await submitResp.json();
if (submitResult.status !== 1) {
throw new Error(`Submit error: ${submitResult.request}`);
}
const taskId = submitResult.request;
// Poll for result
await new Promise(r => setTimeout(r, 15000));
for (let i = 0; i < 24; i++) {
const pollResp = await fetch(
`https://ocr.captchaai.com/res.php?key=${API_KEY}&action=get&id=${taskId}&json=1`
);
const pollResult = await pollResp.json();
if (pollResult.status === 1) return pollResult.request;
if (pollResult.request !== 'CAPCHA_NOT_READY') {
throw new Error(`Solve error: ${pollResult.request}`);
}
await new Promise(r => setTimeout(r, 5000));
}
throw new Error('Solve timeout');
}
// Crawlee spider with CAPTCHA handling
const crawler = new CheerioCrawler({
maxConcurrency: 5,
requestHandlerTimeoutSecs: 180,
async requestHandler({ request, $, log }) {
// Check if page has CAPTCHA
const captchaDiv = $('[data-sitekey]');
if (captchaDiv.length > 0) {
const sitekey = captchaDiv.attr('data-sitekey');
log.info(`CAPTCHA found on ${request.url}, solving...`);
const token = await solveCaptcha(sitekey, request.url);
log.info('CAPTCHA solved, submitting form');
// Submit form with token
const formData = new URLSearchParams({
'g-recaptcha-response': token,
});
const resp = await fetch(request.url, {
method: 'POST',
body: formData,
});
const html = await resp.text();
// Parse the result page...
}
// Extract data
const title = $('title').text();
const data = $('table tr').map((i, row) => ({
col1: $(row).find('td:eq(0)').text().trim(),
col2: $(row).find('td:eq(1)').text().trim(),
})).get();
log.info(`Scraped ${data.length} rows from ${request.url}`);
},
failedRequestHandler({ request, log }) {
log.error(`Failed: ${request.url}`);
},
});
// Run
(async () => {
await crawler.run([
'https://example.com/page1',
'https://example.com/page2',
]);
})();
Zwei Details entscheiden hier über die Stabilität. Erstens muss requestHandlerTimeoutSecs die gesamte Abfrageschleife abdecken: Vorlauf und 24 Abfragen ergeben rund 135 Sekunden, die 180 Sekunden im Beispiel lassen also Luft für Netzwerklatenz. Zweitens gehört jede endgültig gescheiterte URL in den failedRequestHandler, damit ein Nachtlauf am Morgen nachvollziehbar bleibt.
PlaywrightCrawler für JavaScript-gerenderte Seiten
Wird das Widget erst im Browser aufgebaut, steht der Sitekey nicht im ausgelieferten Quelltext. Genau dafür ist der PlaywrightCrawler da: Er öffnet die Seite headless, liest das Attribut per page.evaluate() aus und trägt das gelöste Token danach in das versteckte Feld g-recaptcha-response ein. Entscheidend ist der zweite Schritt – viele Formulare werten das Feld erst aus, wenn zusätzlich der in data-callback hinterlegte Handler aufgerufen wurde. Erst danach absenden.
const { PlaywrightCrawler } = require('crawlee');
const crawler = new PlaywrightCrawler({
maxConcurrency: 3,
requestHandlerTimeoutSecs: 180,
launchContext: {
launchOptions: {
headless: true,
args: ['--disable-blink-features=AutomationControlled'],
},
},
async requestHandler({ request, page, log }) {
await page.goto(request.url, { waitUntil: 'networkidle' });
// Check for reCAPTCHA
const sitekey = await page.evaluate(() => {
const el = document.querySelector('[data-sitekey]');
return el ? el.getAttribute('data-sitekey') : null;
});
if (sitekey) {
log.info(`CAPTCHA detected, solving for ${request.url}`);
const token = await solveCaptcha(sitekey, request.url);
// Inject token
await page.evaluate((t) => {
const ta = document.querySelector('[name="g-recaptcha-response"]');
if (ta) {
ta.style.display = 'block';
ta.value = t;
}
// Trigger callback
const widget = document.querySelector('.g-recaptcha');
if (widget) {
const cb = widget.getAttribute('data-callback');
if (cb && typeof window[cb] === 'function') {
window[cb](t);
}
}
}, token);
await page.click('button[type="submit"]');
await page.waitForNavigation({ waitUntil: 'networkidle' });
}
// Extract data
const title = await page.title();
const content = await page.textContent('body');
log.info(`Page: ${title}, length: ${content.length}`);
},
});
Kalkulieren Sie den höheren Ressourcenbedarf ein: Jeder Browser-Kontext belegt Arbeitsspeicher, deshalb steht maxConcurrency in diesem Beispiel auf 3 statt auf 5.
Session-Pool und die Lebensdauer der Tokens
Bei längeren Läufen lohnt es sich, die Sitzung im Blick zu behalten, in der ein CAPTCHA gelöst wurde. Crawlee verwaltet dafür einen Pool: maxPoolSize begrenzt die Zahl paralleler Sitzungen, maxUsageCount die Zahl der Anfragen pro Sitzung. Legen Sie Token und Lösungszeitpunkt in session.userData ab, dann kann jede Folgeanfrage entscheiden, ob der Wert überhaupt noch brauchbar ist.
const { CheerioCrawler, Session } = require('crawlee');
const crawler = new CheerioCrawler({
useSessionPool: true,
sessionPoolOptions: {
maxPoolSize: 10,
sessionOptions: {
maxUsageCount: 50,
},
},
async requestHandler({ request, $, session, log }) {
// If blocked, solve CAPTCHA and mark session as usable
if ($('.captcha-container').length > 0) {
const sitekey = $('[data-sitekey]').attr('data-sitekey');
const token = await solveCaptcha(sitekey, request.url);
// Store token in session for subsequent requests
session.userData = session.userData || {};
session.userData.captchaToken = token;
session.userData.tokenTime = Date.now();
log.info('CAPTCHA solved, session updated');
}
// Normal scraping
const items = $('div.item').map((i, el) => ({
name: $(el).find('.name').text().trim(),
price: $(el).find('.price').text().trim(),
})).get();
log.info(`Found ${items.length} items`);
},
});
Ein Hinweis, der viel Debugging spart: reCAPTCHA-Token sind nur rund zwei Minuten gültig. Lösen Sie deshalb erst, wenn das Formular ausgefüllt ist, und senden Sie unmittelbar danach ab. Ein auf Vorrat gelöstes Token ist beim Absenden in aller Regel längst abgelaufen.
Fehlercodes und Timeouts sauber behandeln
Die Abfrageschleife in den Beispielen behandelt genau einen Zustand als „normal“: CAPCHA_NOT_READY heißt, die Aufgabe ist noch in Arbeit – hier fragen Sie im festen 5-Sekunden-Takt weiter ab. Alle anderen Rückgaben sind echte Fehler und sollten den Lauf nicht stillschweigend blockieren:
ERROR_WRONG_USER_KEY– der Schlüssel ist ungültig oder falsch formatiert. Prüfen Sie zuerst die Umgebungsvariable im Deployment, nicht den Code.ERROR_ZERO_BALANCE– kein Guthaben beziehungsweise kein freier Thread. Hier hilft nur Aufstocken oder ein Plan mit mehr Threads.ERROR_CAPTCHA_UNSOLVABLE– die Aufgabe kam nicht durch. Sinnvoll ist ein erneuter Versuch mit frisch geladener Seite, nicht mit demselben Sitekey-Snapshot.
Für 5xx-Antworten und Netzwerkfehler eignet sich exponentielles Backoff, für CAPCHA_NOT_READY dagegen ein fester Takt – so bleibt die Lösungszeit kalkulierbar.
Praxisbeispiel: Kategorieseiten eines Shopware-Shops prüfen
Ein Berliner E-Commerce-Team betreibt einen Shopware-Shop und will nach jedem Deployment die eigenen Kategorieseiten auf fehlerhafte Preisdarstellungen prüfen. Der Händlerbereich liegt hinter einem Login mit reCAPTCHA v2, der Lauf startet nachts über GitLab CI und läuft auf einem kleinen Hetzner-VPS.
Der Aufbau ist unspektakulär und genau deshalb belastbar: ein CheerioCrawler mit maxConcurrency: 5 – passend zu den fünf Threads aus BASIC –, zuerst die Login-Seite, danach die Kategorie-URLs aus der Sitemap. Fällt ein CAPTCHA an, löst die oben gezeigte Funktion es, das Token wandert in das Login-Formular, die Sitzung bleibt im Session-Pool und die restlichen Seiten laufen ohne weitere Abfrage durch. Ein nächtlicher Lauf über einige hundert Kategorieseiten kommt so mit einer Handvoll Lösungen aus.
Zwei Punkte, die Teams im deutschsprachigen Raum zusätzlich klären sollten: Sobald Sie Ziele außerhalb der eigenen Infrastruktur abrufen, gelten IP-Adressen und Nutzungsdaten nach DSGVO als personenbezogene Daten – klären Sie Rechtsgrundlage und Speicherdauer, bevor Sie Rohdaten archivieren. Und werfen Sie vorab einen Blick in Nutzungsbedingungen und robots.txt der Zielseite; in vielen deutschen Unternehmen ist genau das der erste Prüfpunkt jeder internen Freigabe.
Häufige Fragen
Wie viele Threads brauche ich für einen Crawlee-Lauf?
So viele, wie gleichzeitig CAPTCHAs offen sind – meist deutlich weniger als maxConcurrency. Für den CheerioCrawler mit fünf parallelen Anfragen reicht BASIC (15 $/Monat, 5 Threads). Erst wenn mehrere Crawler gleichzeitig laufen, wird STANDARD (30 $/Monat, 15 Threads) interessant.
Wie lange bleibt ein gelöstes Token gültig?
Rund zwei Minuten. Lösen Sie daher unmittelbar vor dem Absenden des Formulars. Tokens vorab zu sammeln funktioniert nicht, weil sie beim Einsatz bereits abgelaufen sind.
Lassen sich damit auch hCaptcha oder GeeTest v4 auf Zielseiten bearbeiten?
Nein, beide werden von CaptchaAI derzeit nicht gelöst; GeeTest v4 ist lediglich als bald verfügbar angekündigt. Abgedeckt sind unter anderem reCAPTCHA v2 und v3 samt Enterprise-Varianten, Cloudflare Turnstile und Challenge, GeeTest v3 sowie Bild- und Rasterbild-CAPTCHAs. CaptchaFox, Friendly Captcha und Lemin befinden sich in der Beta.
Funktioniert dasselbe Muster in TypeScript?
Ja. Crawlee ist selbst in TypeScript geschrieben; solveCaptcha bekommt lediglich Typen für Sitekey, Page-URL und Rückgabewert. An den API-Aufrufen ändert sich nichts.
Wohin gehört die Lösungsroutine im Projekt?
In ein eigenes Modul, nicht in den requestHandler. So teilen sich CheerioCrawler, PlaywrightCrawler und mögliche Hintergrundjobs dieselbe Implementierung, und Timeouts, Wiederholungslogik oder Fehlerbehandlung ändern Sie an genau einer Stelle.
Weiterlesen
Ihr Crawlee-Lauf soll an CAPTCHA-Seiten nicht mehr hängen bleiben? Holen Sie sich Ihren API-Schlüssel bei CaptchaAI.