So handhaben Sie Captchas in Crawlee mit dem Node.js SDK

Crawlee hat keinen Captcha-Hook und braucht auch keinen. Ein Crawlee-Captcha lösen Sie in Ihrem requestHandler, mitten in der Anfrage, für die Sie bereits eine Browser-Seite haben. Drei Dinge sind dafür nötig: Erkennen Sie das Widget, bevor Sie eine Lösung dafür verbrauchen, erhöhen Sie das Handler-Timeout, weil der Standardwert kürzer ist als ein reCAPTCHA-Solve, und werfen Sie bei einem Fehlschlag eine Exception, damit Crawlee die Anfrage über seine eigene Warteschlange wiederholt statt über Ihre Schleife. Dieser Leitfaden zeigt alle drei Punkte am Beispiel von PlaywrightCrawler.
Was Sie brauchen
- Node.js 18 oder neuer und ein Crawlee-Projekt, das bereits etwas crawlt
- CapSkip läuft und ist erreichbar. Der Local-Modus lauscht auf 127.0.0.1 Port 8080 für Automatisierung auf demselben Rechner, und der Server-Modus lauscht auf Ihrer Netzwerk- oder öffentlichen IP, sodass ein Crawler auf einer anderen Maschine, einem VPS oder einem Container-Host ihn aufrufen kann. Beides finden Sie in den Verbindungseinstellungen
- Die drei Pakete, gemeinsam installiert
# One install for the crawler, the browser and the solver client. npm install crawlee playwright capskip # Crawlee drives a real browser, so fetch one. npx playwright install chromium
Die Beispiele hier sind CommonJS, also die Form, die die CapSkip-README dokumentiert. Crawlee 3 liefert beide Builds aus, ein ESM-Projekt kann für den Crawler also import-Anweisungen verwenden.
Wohin das Lösen gehört: in den requestHandler
Scrapy hat Downloader-Middleware und Selenium hat den Wrapper, den Sie sich selbst gebaut haben. Crawlee gibt Ihnen das page-Objekt direkt, es gibt also keine Abfangschicht zu schreiben. Sie erkennen die Challenge, lösen sie und machen in derselben Funktion weiter.
Erst erkennen. Auf jeder Seite einen Solve abzufeuern verbraucht Kapazität für Seiten, die nie eine Challenge gestellt haben, und es verdeckt das nützliche Signal, wie oft Sie tatsächlich blockiert werden.
// npm install crawlee playwright capskip
const { PlaywrightCrawler } = require('crawlee');
const { CapSkip } = require('capskip');
// Local mode. Point host at a server IP to share one solver.
const solver = new CapSkip({ host: '127.0.0.1', port: 8080 });
async function solveIfChallenged(page, url, log) {
const widget = page.locator('[data-sitekey]').first();
if ((await widget.count()) === 0) return false;
const sitekey = await widget.getAttribute('data-sitekey');
log.info(`Solving sitekey ${sitekey}`);
const result = await solver.recaptcha(sitekey, url);
return result.code; // the token
}Das Attribut data-sitekey steht bei reCAPTCHA v2 im Widget-div und ebenso im Turnstile-div, weshalb ein einziger Selektor beides abdeckt. reCAPTCHA v3 hat kein sichtbares Widget, den Key lesen Sie dort stattdessen aus der Skript-URL.
Token einspeisen, dann absenden
Das Lösen liefert Ihnen ein Token. Die Seite erwartet dieses Token weiterhin in dem versteckten Feld, das ihr eigenes Widget gefüllt hätte, setzen Sie es also dorthin und senden Sie das Formular so ab, wie es ein Browser tun würde.
// The widget writes into a hidden textarea. Do the same.
await page.evaluate((token) => {
const field = document.getElementById('g-recaptcha-response');
field.value = token;
}, token);
// Then submit exactly as the page would, and wait for the result.
await Promise.all([
page.waitForNavigation(),
page.click('button[type=submit]'),
]);Manche Seiten rufen einen JavaScript-Callback auf, statt ein Formular abzuschicken. Trägt das Widget-div ein data-callback Attribut, rufen Sie diese Funktion mit dem Token auf, statt irgendwo zu klicken, denn der Klick-Handler wird unter Umständen nie ausgeführt.
requestHandlerTimeoutSecs zuerst erhöhen, vor allem anderen
Das ist der Punkt, an dem es die meisten erwischt, und es sieht nach einem Problem des Lösers aus, obwohl es keines ist.
In der Voreinstellung gibt PlaywrightCrawler jedem Request Handler 60 Sekunden Zeit. Ein reCAPTCHA-v2-Job ist in den ersten 15 bis 20 Sekunden nicht fertig, v3 braucht 10 bis 15, und das ist, bevor Sie die Seite geladen, das Token eingespeist und auf eine Navigation gewartet haben. Der Handler wird mitten im Lösen abgebrochen, Crawlee protokolliert einen Timeout, und die Anfrage geht zurück in die Warteschlange, um alles noch einmal zu durchlaufen.
// npm install crawlee playwright capskip
const crawler = new PlaywrightCrawler({
// 60 is the default and it is shorter than a v2 solve plus a submit.
requestHandlerTimeoutSecs: 180,
// Three tries per URL, which is Crawlee's default and the right one.
maxRequestRetries: 3,
async requestHandler({ page, request, log }) {
// your handler
},
});180 Sekunden sind eine sinnvolle Obergrenze. Das ist etwa das Zehnfache eines normalen Lösevorgangs und liegt bewusst unter dem 300-Sekunden-Polling-Limit, das das SDK selbst für reCAPTCHA setzt. So gibt Crawlee eine wirklich hängende Anfrage auf, statt sie einen Browser-Slot volle fünf Minuten belegen zu lassen. Wenn stattdessen der Solver-Client zuerst aufgeben soll, setzen Sie recaptchaTimeout auf einen Wert unter Ihrem Handler-Timeout.
Vollständiges lauffähiges Beispiel
Eine Datei, ein Crawler, ein Lösungspfad. Tragen Sie im run-Aufruf Ihre eigene Start-URL ein.
// npm install crawlee playwright capskip
const { PlaywrightCrawler, Dataset } = require('crawlee');
const { CapSkip } = require('capskip');
const solver = new CapSkip({ host: '127.0.0.1', port: 8080 });
const crawler = new PlaywrightCrawler({
requestHandlerTimeoutSecs: 180,
maxRequestRetries: 3,
async requestHandler({ page, request, log }) {
const widget = page.locator('[data-sitekey]').first();
if ((await widget.count()) > 0) {
const sitekey = await widget.getAttribute('data-sitekey');
const result = await solver.recaptcha(sitekey, request.loadedUrl);
await page.evaluate((token) => {
document.getElementById('g-recaptcha-response').value = token;
}, result.code);
await Promise.all([
page.waitForNavigation(),
page.click('button[type=submit]'),
]);
log.info(`Cleared the challenge on ${request.loadedUrl}`);
}
await Dataset.pushData({ url: request.loadedUrl, title: await page.title() });
},
});
await crawler.run(['https://example.com/page-with-recaptcha']);Der Solve läuft auf Ihrer eigenen Maschine, das Retry-Budget oben kostet also nichts außer Laufzeit. Das ist der praktische Unterschied zu einem abgerechneten Dienst, bei dem drei Versuche pro URL ein Kostenposten sind.
Lassen Sie die Warteschlange wiederholen, bauen Sie keine eigene Schleife
Der Reflex ist, den Solve in eine for-Schleife zu packen. Tun Sie das nicht. Crawlee hat bereits ein Retry-System, das die Request-Queue, den Session-Pool und die Proxy-Konfiguration kennt, und eine selbstgebaute Schleife im Handler ist für alle drei unsichtbar.
Werfen Sie stattdessen eine Exception. Ein Handler, der wirft, schickt die Anfrage zurück in die Warteschlange, und Crawlee wiederholt sie bis zu maxRequestRetries Mal mit einem frischen Browser-Kontext.
// npm install capskip
const { ApiException, NetworkException, TimeoutException } = require('capskip');
const crawler = new PlaywrightCrawler({
requestHandlerTimeoutSecs: 180,
// Runs between retries, while attempts remain.
errorHandler({ request, log }, error) {
log.warning(`Retry ${request.retryCount} for ${request.url}: ${error.message}`);
},
// Runs once, after the last attempt fails.
failedRequestHandler({ request, log }) {
log.error(`Gave up on ${request.url}`);
},
});Welche Exception herauskommt, sagt Ihnen, was Sie ändern müssen. Eine NetworkException bedeutet, dass CapSkip nicht erreichbar war, prüfen Sie also Host und Port, bevor Sie die Seite verantwortlich machen. Eine TimeoutException bedeutet, dass das Polling-Fenster abgelaufen ist und die Seite wahrscheinlich eine härtere Challenge ausliefert, als Sie denken. Eine ApiException trägt einen zurückgegebenen Fehlercode, und das ist die eine, die Sie zusammen mit der URL protokollieren sollten.
Crawler und Löser auf verschiedenen Maschinen betreiben
Crawlee skaliert, indem mehr Instanzen von sich selbst laufen, und in einer Crawl-Flotte auf getrennten Maschinen können nicht alle mit 127.0.0.1 sprechen. Der Server-Modus ist die Antwort: CapSkip lauscht auf Ihrer Netzwerk- oder öffentlichen IP statt auf dem Loopback, und jeder Worker zeigt auf dieselbe Adresse.
// npm install capskip
const { CapSkip } = require('capskip');
// Same client, different address. Nothing else in the code changes.
const solver = new CapSkip({
host: process.env.CAPSKIP_HOST || '127.0.0.1',
port: Number(process.env.CAPSKIP_PORT || 8080),
});Das SDK liest CAPSKIP_HOST und CAPSKIP_PORT von sich aus aus der Umgebung, der Fallback oben ist also eine doppelte Absicherung für einen Container, der ohne sie startet. Für die Löser-Maschine wird eine statische öffentliche IP empfohlen, und die Einrichtungsschritte stehen in den Verbindungseinstellungen. Es bleibt Ihre eigene Hardware und bleibt ohne verbrauchsabhängige Abrechnung, geändert hat sich also nur, wo der Prozess läuft.
Häufige Fehler und was sie bedeuten
| Symptom | Ursache | Beheben |
|---|---|---|
| requestHandler läuft nach 60 s in einen Timeout | Das Standard-Timeout des Handlers ist kürzer als ein Solve | requestHandlerTimeoutSecs auf 180 setzen |
| Solve gelingt, Seite blockiert trotzdem | Das Token wurde eingesetzt, aber das Formular wurde nie abgeschickt | Prüfen Sie, ob ein data-callback-Attribut vorhanden ist, und rufen Sie es auf |
ERROR_GOOGLEKEY | Das sitekey-Attribut war leer oder wurde vom falschen Element gelesen | Protokollieren Sie den Wert vor dem Lösen; v3-Keys stehen in der Skript-URL |
ERROR_PAGEURL | Der Handler hat eine relative oder umgeleitete URL übergeben | Verwenden Sie request.loadedUrl, also die URL nach den Weiterleitungen |
| NetworkException bei jeder Anfrage | Der Crawler erreicht den Löser nicht | Der Local-Modus ist nur Loopback; wechseln Sie für einen entfernten Worker in den Server-Modus |
| Jede URL dreimal wiederholt, dann verworfen | Der Handler wirft schon vor dem Solve | Lesen Sie das errorHandler-Log; der erste Fehlschlag ist der eigentliche |
Die Parameternamen und die vollständige Liste der Fehlercodes stehen in der API-Dokumentation.
FAQ
Funktioniert das mit CheerioCrawler?
Teilweise. CheerioCrawler hat keinen Browser, es gibt also kein page-Objekt und keine Möglichkeit, das eigene JavaScript des Widgets auszuführen. Sie können den sitekey trotzdem aus dem HTML parsen, ihn lösen und das Token selbst mit dem Formular-Body absenden. Das reicht für ein einfaches Formular-Submit und nicht für alles, was einen Callback erwartet. Verwenden Sie PlaywrightCrawler, wenn eine Challenge wahrscheinlich ist.
Sollte ich stattdessen in einem preNavigationHook lösen?
Nein. Pre-Navigation-Hooks laufen, bevor die Seite geladen ist, es gibt also noch nichts zu erkennen. Post-Navigation-Hooks kommen näher heran, aber im Request Handler haben Sie die Seite, die URL nach den Weiterleitungen und den Logger bereits zur Hand. Lassen Sie das Lösen dort und behalten Sie die Hooks für Cookies und Header.
Kann der Crawler auf einer gehosteten Plattform laufen, während der Löser zu Hause bleibt?
Ja, mit CapSkip im Server-Modus. Der Crawler braucht eine Route zur Adresse des Lösers, ein Heimanschluss braucht also eine statische öffentliche IP und einen offenen Port, und ein VPS ist die einfachere Option. Der Client-Code ist in beiden Fällen identisch: Nur der Host-Wert ändert sich.
Wie viele gleichzeitige Lösungen kann ein Crawl anstoßen?
Crawlee skaliert seine eigene Nebenläufigkeit automatisch, und jeder Handler wartet unabhängig auf seine eigene Lösung, es gibt auf der Client-Seite also keine Warteschlange zu konfigurieren. Das SDK beginnt mit dem Polling bei 250 Millisekunden und geht bis zur Obergrenze pollingInterval zurück, was ein schnelles Lösen auch dann schnell hält, wenn mehrere gleichzeitig unterwegs sind. Richten Sie Ihre Crawlee-Nebenläufigkeit danach aus, was die Zielseite verträgt, nicht nach dem Löser.
Die Kurzfassung
Erkennen Sie das Widget, lösen Sie im Request Handler, erhöhen Sie das Handler-Timeout auf 180 Sekunden und werfen Sie eine Exception, damit die Warteschlange den Versuch wiederholt. Dass Sie den Löser selbst betreiben, macht drei Versuche zu einer vernünftigen Voreinstellung statt zu einer Kostenentscheidung, und das ist dasselbe Argument dafür, überall in einem Crawl eine lokale Captcha-Umgehung einzusetzen. Der Node.js-Integrationsleitfaden behandelt das Client-Setup, der Playwright-Leitfaden enthält die browserseitigen Details, die Crawlee erbt, und Captcha-Lösen für Web Scraping behandelt das Session-Handling über einen ganzen Crawl hinweg. Für dasselbe Muster in Python siehe den Beitrag zur Scrapy-Middleware.
