Tutorials

Node.js Playwright + CaptchaAI Vollständige Integration

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:

  1. Auslesen – den sitekey (bei reCAPTCHA googlekey) und die pageurl aus dem geladenen DOM extrahieren.
  2. Übermitteln – die Parameter per POST an in.php schicken und eine Task-ID erhalten.
  3. Abfragenres.php im Sekundentakt pollen, bis das Token vorliegt (Polling, keine Umfrage).
  4. 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 fetch nativ 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.

Verwandte Artikel

Kommentare sind für diesen Artikel deaktiviert.