Playwright füllt Formulare aus, klickt und navigiert zuverlässig – aber ein reCAPTCHA-Feld oder ein Cloudflare-Turnstile-Widget löst der Browser nicht von allein. Der praktikable Weg führt über eine Löser-API: Sie lesen den Sitekey aus der Seite aus, übergeben ihn an CaptchaAI, fragen das Ergebnis ab und tragen das fertige Token in das Formular ein. Dieser Leitfaden zeigt die komplette Integration in eine Node.js-Playwright-Automatisierung – von der reCAPTCHA-v2- und Turnstile-Erkennung über die automatische Typ-Erkennung bis zur einsatzfertigen Automatisierungsklasse.
So läuft die Integration im Überblick
Jede CAPTCHA-Lösung folgt in Playwright demselben Vier-Schritt-Muster, unabhängig vom Typ:
- Auslesen – den
sitekey(bei reCAPTCHAgooglekey) und diepageurlaus dem geladenen DOM extrahieren. - Übermitteln – die Parameter per POST an
in.phpschicken und eine Task-ID erhalten. - Abfragen –
res.phpim Sekundentakt pollen, bis das Token vorliegt (Polling, keine Umfrage). - Einfügen – das Token in das versteckte Antwortfeld eintragen und gegebenenfalls den Callback auslösen.
Wer dieses Muster einmal kapselt, ruft es für jeden CAPTCHA-Typ gleich auf – genau das baut dieser Leitfaden Schritt für Schritt auf.
Voraussetzungen
Für die Integration brauchen Sie drei Dinge:
- Node.js 18 oder neuer – bringt
fetchnativ mit, sodass keine zusätzliche HTTP-Bibliothek nötig ist. - Playwright samt Chromium-Browser.
- einen CaptchaAI-API-Schlüssel von captchaai.com.
npm install playwright
npx playwright install chromium
Playwright-Browser einrichten
Ein Kontext mit realistischem User-Agent und passender Viewport-Größe sorgt für stabiles Verhalten auf produktiven Seiten. Das Flag --disable-blink-features=AutomationControlled unterdrückt einen der auffälligsten Automatisierungshinweise.
const { chromium } = require("playwright");
async function createBrowser() {
const browser = await chromium.launch({
headless: false,
args: ["--disable-blink-features=AutomationControlled"],
});
const context = await browser.newContext({
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
viewport: { width: 1920, height: 1080 },
locale: "en-US",
});
const page = await context.newPage();
return { browser, context, page };
}
Die zentrale Löser-Funktion
Diese Funktion kapselt Übermittlung und Polling. Sie schickt die Anfragedaten an in.php, liest die Task-ID aus und fragt res.php bis zu 30-mal im Fünf-Sekunden-Takt ab. Liefert die API ERROR_CAPTCHA_UNSOLVABLE, wird der Versuch sauber abgebrochen statt endlos zu warten.
const API_KEY = "YOUR_API_KEY";
async function solveCaptcha(method, params) {
// Submit
const submitResp = await fetch("https://ocr.captchaai.com/in.php", {
method: "POST",
body: new URLSearchParams({ key: API_KEY, method, json: "1", ...params }),
});
const submitData = await submitResp.json();
if (submitData.status !== 1) throw new Error(`Submit: ${submitData.request}`);
const taskId = submitData.request;
// Poll
for (let i = 0; i < 30; i++) {
await new Promise((r) => setTimeout(r, 5000));
const pollResp = await fetch(
`https://ocr.captchaai.com/res.php?${new URLSearchParams({
key: API_KEY,
action: "get",
id: taskId,
json: "1",
})}`
);
const data = await pollResp.json();
if (data.status === 1) return data.request;
if (data.request === "ERROR_CAPTCHA_UNSOLVABLE") throw new Error("Unsolvable");
}
throw new Error("Timed out");
}
reCAPTCHA v2 in Playwright lösen
Der Sitekey steckt im Attribut data-sitekey. Nach dem Lösen wird das Token in das versteckte g-recaptcha-response-Feld geschrieben. Entscheidend ist der letzte Schritt: Viele Seiten reagieren erst, wenn der reCAPTCHA-Callback explizit aufgerufen wird – deshalb durchläuft der Code die registrierten Clients und feuert die Callback-Funktion.
async function solveRecaptchaV2(page) {
// Extract sitekey
const sitekey = await page.evaluate(() => {
const el = document.querySelector("[data-sitekey]");
return el ? el.getAttribute("data-sitekey") : null;
});
if (!sitekey) throw new Error("Sitekey not found");
// Solve
const token = await solveCaptcha("userrecaptcha", {
googlekey: sitekey,
pageurl: page.url(),
});
// Inject
await page.evaluate((t) => {
const textarea = document.getElementById("g-recaptcha-response");
if (textarea) {
textarea.value = t;
textarea.style.display = "block";
}
// Trigger callback
if (typeof ___grecaptcha_cfg !== "undefined") {
const clients = ___grecaptcha_cfg.clients;
for (const key in clients) {
for (const prop in clients[key]) {
try {
const cb = clients[key][prop];
if (cb && typeof cb.callback === "function") cb.callback(t);
} catch {}
}
}
}
}, token);
return token;
}
Cloudflare Turnstile in Playwright lösen
Turnstile-Sitekeys beginnen mit 0x. Der Extraktions-Code prüft zuerst das .cf-turnstile-Element und fällt andernfalls auf jedes data-sitekey-Attribut mit passendem Präfix zurück. Das Token landet anschließend in allen cf-turnstile-response-Feldern der Seite.
async function solveTurnstile(page) {
// Extract sitekey
const sitekey = await page.evaluate(() => {
const el = document.querySelector(".cf-turnstile[data-sitekey]");
if (el) return el.getAttribute("data-sitekey");
// Fallback: any data-sitekey starting with 0x
const all = document.querySelectorAll("[data-sitekey]");
for (const item of all) {
const key = item.getAttribute("data-sitekey");
if (key && key.startsWith("0x")) return key;
}
return null;
});
if (!sitekey) throw new Error("Turnstile sitekey not found");
// Solve
const token = await solveCaptcha("turnstile", {
sitekey,
pageurl: page.url(),
});
// Inject
await page.evaluate((t) => {
document
.querySelectorAll('[name="cf-turnstile-response"]')
.forEach((el) => (el.value = t));
}, token);
return token;
}
CAPTCHA-Typ automatisch erkennen und lösen
In der Praxis wissen Sie oft nicht im Voraus, welcher CAPTCHA-Typ eine Seite erwartet. Diese Funktion prüft das DOM auf reCAPTCHA, Turnstile und Bild-CAPTCHA und wählt automatisch die passende Löser-Methode.
Das ist der Kern einer wartungsarmen Automatisierung: eine Erkennungslogik statt fest verdrahteter Sonderfälle pro Zielseite.
async function detectAndSolve(page) {
const captchaInfo = await page.evaluate(() => {
// Check reCAPTCHA
const recaptcha = document.querySelector("[data-sitekey]");
if (
recaptcha &&
(document.querySelector(".g-recaptcha") ||
document.querySelector('script[src*="recaptcha"]'))
) {
return { type: "recaptcha", sitekey: recaptcha.getAttribute("data-sitekey") };
}
// Check Turnstile
const turnstile = document.querySelector(".cf-turnstile[data-sitekey]");
if (turnstile) {
return { type: "turnstile", sitekey: turnstile.getAttribute("data-sitekey") };
}
// Check image CAPTCHA
const captchaImg = document.querySelector(
'img.captcha, img[alt*="captcha"], img[src*="captcha"]'
);
if (captchaImg) {
return { type: "image" };
}
return { type: null };
});
if (!captchaInfo.type) return null;
console.log(`Detected: ${captchaInfo.type}`);
switch (captchaInfo.type) {
case "recaptcha":
return await solveCaptcha("userrecaptcha", {
googlekey: captchaInfo.sitekey,
pageurl: page.url(),
});
case "turnstile":
return await solveCaptcha("turnstile", {
sitekey: captchaInfo.sitekey,
pageurl: page.url(),
});
case "image":
return await solveImageCaptcha(page);
default:
return null;
}
}
Bild-CAPTCHA per Screenshot lösen
Klassische Bild-CAPTCHAs haben keinen Sitekey. Playwright macht stattdessen einen Screenshot des Elements, der als Base64 an CaptchaAI geht (method: base64). Die zurückgegebene Zeichenkette wird direkt in das Eingabefeld getippt.
async function solveImageCaptcha(page) {
const captchaImg = page.locator(
'img.captcha, img[alt*="captcha"], img[src*="captcha"]'
).first();
// Screenshot the CAPTCHA element
const imgBuffer = await captchaImg.screenshot();
const imgBase64 = imgBuffer.toString("base64");
// Solve via CaptchaAI
const answer = await solveCaptcha("base64", { body: imgBase64 });
// Type the answer
const input = page.locator(
'input[name="captcha"], input[name="code"], input.captcha-input'
).first();
await input.fill(answer);
return answer;
}
GeeTest-v3-Parameter über Route-Interception auslesen
Manche CAPTCHA-Parameter stehen nicht im HTML, sondern kommen erst über einen XHR-Aufruf zurück. Playwrights routenbasiertes Interception liest die Antworten mit und greift die GeeTest-Werte gt und challenge ab, sobald sie eintreffen. CaptchaAI unterstützt GeeTest v3; die v4-Variante ist noch nicht verfügbar.
async function interceptCaptchaRoutes(page, url) {
const captchaParams = {};
// Intercept responses
page.on("response", async (response) => {
const respUrl = response.url();
// GeeTest parameters
if (respUrl.includes("geetest") || respUrl.includes("gt=")) {
try {
const data = await response.json();
if (data.gt) {
captchaParams.type = "geetest";
captchaParams.gt = data.gt;
captchaParams.challenge = data.challenge;
}
} catch {}
}
});
await page.goto(url, { waitUntil: "networkidle" });
return captchaParams;
}
Die komplette Automatisierungsklasse
Alle Bausteine zusammengefasst ergeben eine wiederverwendbare Klasse. Sie kapselt Browser-Start, Navigation, Formular-Handling und den kompletten Login-mit-CAPTCHA-Ablauf hinter einer schlanken Schnittstelle.
Private Felder (#browser, #page) halten den Zustand intern; loginWithCaptcha() orchestriert den gesamten Vorgang in einem Aufruf.
const { chromium } = require("playwright");
class PlaywrightAutomation {
#apiKey;
#browser;
#context;
#page;
constructor(apiKey) {
this.#apiKey = apiKey;
}
async start(headless = false) {
this.#browser = await chromium.launch({
headless,
args: ["--disable-blink-features=AutomationControlled"],
});
this.#context = await this.#browser.newContext({
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36",
viewport: { width: 1920, height: 1080 },
});
await this.#context.addInitScript(() => {
Object.defineProperty(navigator, "webdriver", { get: () => undefined });
});
this.#page = await this.#context.newPage();
}
async stop() {
await this.#browser?.close();
}
async navigate(url) {
await this.#page.goto(url, { waitUntil: "networkidle" });
}
async fillForm(fields) {
for (const [selector, value] of Object.entries(fields)) {
await this.#page.fill(selector, value);
}
}
async solveCaptcha() {
return await detectAndSolve(this.#page);
}
async submit(selector = 'button[type="submit"]') {
await this.#page.click(selector);
await this.#page.waitForLoadState("networkidle");
return this.#page.url();
}
async loginWithCaptcha(url, fields, submitSelector) {
await this.navigate(url);
await this.fillForm(fields);
const token = await this.solveCaptcha();
if (token) {
// Inject token
await this.#page.evaluate((t) => {
const re = document.getElementById("g-recaptcha-response");
if (re) re.value = t;
document
.querySelectorAll('[name="cf-turnstile-response"]')
.forEach((el) => (el.value = t));
}, token);
}
return await this.submit(submitSelector);
}
get page() {
return this.#page;
}
}
// Usage
const bot = new PlaywrightAutomation("YOUR_API_KEY");
await bot.start();
try {
const result = await bot.loginWithCaptcha(
"https://example.com/login",
{
"#email": "user@example.com",
"#password": "pass123",
},
"#login-btn"
);
console.log(`Redirected to: ${result}`);
} finally {
await bot.stop();
}
Welche CAPTCHA-Typen CaptchaAI abdeckt
Der obige Code deckt drei Familien ab (reCAPTCHA, Turnstile, Bild) – die API kann mehr. Generell verfügbar sind:
- reCAPTCHA v2 und v3, inklusive der Enterprise-Varianten
- Cloudflare Turnstile und Cloudflare Challenge
- GeeTest v3
- Bild-/OCR-CAPTCHAs, Rasterbild-CAPTCHAs und BLS-CAPTCHAs
CaptchaFox, Friendly Captcha und Lemin befinden sich in der Beta-Phase und sollten nur als solche eingeplant werden.
Wichtig für ehrliche Planung: hCaptcha und FunCaptcha (Arkose Labs) werden nicht unterstützt, und GeeTest v4 ist noch nicht verfügbar (bald verfügbar). Wenn eine Zielseite auf einen dieser Typen setzt, brauchen Sie eine andere Strategie – die hier gezeigte Erkennungslogik gibt für unbekannte Typen sauber null zurück, statt einen Fehlversuch zu erzwingen.
Thread-basierte Abrechnung statt Preis pro Lösung
CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro einzelner Lösung. Ein Thread ist ein CAPTCHA in Bearbeitung; sobald es gelöst ist, nimmt derselbe Thread das nächste an. Jeder Tarif enthält unbegrenzte Lösungen pro Thread im Abrechnungsmonat – es gibt keine Tageslimits und keine Aufschläge nach CAPTCHA-Typ. Ihr Durchsatz hängt also nur von der Lösungszeit pro Typ und Ihrer Thread-Zahl ab.
Ein Beispiel aus der DACH-Praxis: Ein nächtlicher Scraper läuft auf einem Hetzner-Cloud-Server in Nürnberg und löst pro Lauf einige Hundert Turnstile-Abfragen sequenziell – dafür genügt der BASIC-Tarif (15 $/Monat, 5 Threads). Verlagert das Team die Pipeline in GitLab CI und lässt mehrere Jobs parallel laufen, passt eher STANDARD (30 $/Monat, 15 Threads) oder ADVANCE (90 $/Monat, 50 Threads). Abgerechnet wird ausschließlich in US-Dollar.
DSGVO-Hinweis: IP-Adressen gelten als personenbezogene Daten – prüfen Sie beim Scraping Ihre Rechtsgrundlage.
Playwright und Puppeteer im Vergleich
| Funktion | Playwright | Puppeteer |
|---|---|---|
| Multi-Browser | Chromium, Firefox, WebKit | Nur Chromium |
| API-Stil | Locator-basiert | Selektorbasiert |
| Automatisches Warten | Integriert | Manuelle Wartezeiten |
| Netzwerk-Interception | Route-basiert | Request-basiert |
| Browser-Konfiguration | Gute Standardwerte | Erfordert manuelle Flags |
| TypeScript | Nativ | Community-Typen |
Fehlerbehebung
Die häufigsten Stolpersteine bei der Playwright-Integration und ihre Ursachen:
| Symptom | Ursache | Behebung |
|---|---|---|
page.evaluate gibt null zurück |
Element noch nicht geladen | Zuerst waitForSelector verwenden |
| Turnstile nicht erkannt | Wird nach dem Seitenaufbau per JS nachgeladen | Auf den .cf-turnstile-Selektor warten |
| Token-Einfügung wird nicht übermittelt | Callback-Trigger fehlt | Den reCAPTCHA-Callback explizit aufrufen |
| Browsererkennung | Init-Skript fehlt | Webdriver-Überschreibung ergänzen |
networkidle-Timeout |
Skripte mit langem Polling | Stattdessen domcontentloaded nutzen |
Häufige Fragen
Löst Playwright CAPTCHAs von selbst?
Nein. Playwright steuert nur den Browser – es erkennt oder löst keine CAPTCHAs. Das Lösen übernimmt die CaptchaAI-API; Playwright liest den Sitekey aus und fügt das gelöste Token wieder ein.
Welche CAPTCHA-Typen deckt diese Integration ab?
reCAPTCHA v2 und v3, Cloudflare Turnstile sowie Bild-CAPTCHAs sind direkt im Code enthalten. Über dieselbe Löser-Funktion lassen sich außerdem GeeTest v3, Cloudflare Challenge und BLS-CAPTCHAs ansprechen. hCaptcha und FunCaptcha werden nicht unterstützt.
Funktioniert die Integration im Headless-Modus?
Ja. Setzen Sie headless: true in launch(). Da CaptchaAI unabhängig vom Browser löst, hat der Headless-Modus keinen Einfluss auf den Lösungserfolg.
Was kostet das Lösen bei hohem Volumen?
Die Abrechnung erfolgt pro gleichzeitigem Thread, nicht pro Lösung. BASIC startet bei 15 $/Monat mit 5 Threads und unbegrenzten Lösungen; für parallele Pipelines skalieren STANDARD (30 $/Monat, 15 Threads) und ADVANCE (90 $/Monat, 50 Threads).
Wie lange ist ein gelöstes Token gültig?
Nur wenige Minuten. Fügen Sie das Token direkt nach dem Abfragen ein und senden Sie das Formular ab, statt es zwischenzuspeichern – abgelaufene Token werden von der Zielseite abgelehnt.
Fazit
Node.js Playwright + CaptchaAI ergibt einen modernen Automatisierungs-Stack mit automatischer Erkennung, Route-Interception und Unterstützung mehrerer CAPTCHA-Typen. Das Vier-Schritt-Muster – auslesen, übermitteln, abfragen, einfügen – bleibt für jeden Typ identisch, und die Klasse PlaywrightAutomation bündelt den kompletten Login-mit-CAPTCHA-Workflow hinter einem einzigen Aufruf.