Tutorials

Полная интеграция Node.js Playwright + CaptchaAI

Когда Playwright во время автоматизации упирается в проверку CAPTCHA, схема всегда одна: со страницы извлекается sitekey, отправляется в CaptchaAI, а готовый токен подставляется обратно в форму. Ниже собрана полная интеграция на Node.js — единый решатель, автоопределение типа капчи и класс, который проводит вход целиком.

Код рассчитан на реальный сценарий: команда прогоняет форму входа на staging-стенде из региона Франкфурта или Алматы, а res.php опрашивается с фиксированным интервалом, чтобы нестабильная сеть не роняла прогон. Работает интеграция на поддерживаемых типах CAPTCHA — reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest v3 и image/OCR.

Что вы соберёте по ходу руководства:

  • единый решатель CaptchaAI на fetch с опросом res.php;
  • отдельные обработчики для reCAPTCHA v2, Turnstile и image-капчи;
  • автоопределение типа капчи прямо в разметке страницы;
  • класс PlaywrightAutomation, проводящий вход целиком.

Playwright или Puppeteer: с чего начать

Если стек ещё не выбран, начните с этой сводки. Playwright из коробки поддерживает несколько браузеров и TypeScript, тогда как Puppeteer исторически заточен под Chromium — для интеграции с CaptchaAI это влияет на то, сколько ручной настройки понадобится.

Характеристика Playwright Puppeteer
Мультибраузерность Chromium, Firefox, WebKit Только Chromium
Стиль API На основе локаторов На основе селекторов
Автоожидание Встроенное Ручные ожидания
Перехват сети По маршрутам По запросам
Совместимость с проверками Хорошие настройки по умолчанию Нужен отдельный плагин
TypeScript Нативный Типы от сообщества

Установка и зависимости

Перед запуском убедитесь, что готово следующее:

  • Node.js 18+ и менеджер пакетов npm;
  • API-ключ CaptchaAI из личного кабинета;
  • доступ к тестовой странице с формой (staging, не боевой сайт).

Playwright ставится одной командой; браузер Chromium загружается отдельно.

npm install playwright
npx playwright install chromium

Запуск браузера для стабильной автоматизации

Здесь задаётся контекст с предсказуемым User-Agent, фиксированным viewport и локалью — так поведение прогона совпадает между локальной машиной и CI. Инициализационный скрипт приводит окружение к обычному браузеру, чтобы формы и виджеты вели себя штатно.

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",
  });

  // Remove Playwright detection
  await context.addInitScript(() => {
    Object.defineProperty(navigator, "webdriver", { get: () => undefined });
    delete navigator.__proto__.webdriver;
  });

  const page = await context.newPage();
  return { browser, context, page };
}

Универсальный решатель CaptchaAI

Одна функция закрывает весь цикл из трёх шагов:

  1. POST на in.php отправляет задачу и возвращает её ID;
  2. res.php опрашивается раз в 5 секунд (до 30 попыток) до готового результата;
  3. при ERROR_CAPTCHA_UNSOLVABLE цикл прерывается сразу, не дожидаясь тайм-аута.

Тарификация у CaptchaAI идёт по потокам, а не за решение, поэтому параллельные вкладки Playwright ограничены только числом потоков вашего тарифа — например, BASIC ($15/мес, 5 потоков).

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 в Playwright

Порядок такой: находим data-sitekey на странице, отправляем его методом userrecaptcha, а полученный токен кладём в скрытое поле g-recaptcha-response. Одного значения поля часто мало — многие формы ждут вызова JS-колбэка, поэтому код дополнительно триггерит callback виджета.

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 в Playwright

Turnstile нередко подгружается через JS уже после загрузки страницы, а его sitekey начинается с 0x. Поэтому извлечение идёт с запасным вариантом:

  • сначала ищем элемент .cf-turnstile[data-sitekey];
  • если его нет — перебираем все data-sitekey и берём тот, что начинается с 0x.

Полученный токен подставляется во все поля cf-turnstile-response.

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;
}

Автоопределение типа капчи

Если заранее неизвестно, какая проверка стоит на странице, тип определяется по разметке. Функция сама выбирает метод и возвращает готовый токен, а неизвестные виджеты просто отдают null. Признаки, по которым различаются типы:

  • reCAPTCHA — пара data-sitekey и .g-recaptcha (метод userrecaptcha);
  • Cloudflare Turnstile — класс .cf-turnstile (метод turnstile);
  • image-капча — тег img с признаком captcha (метод base64).
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;
  }
}

Решение image-капчи по скриншоту

Для картиночной капчи Playwright делает скриншот именно элемента, кодирует его в base64 и передаёт методом base64. Ответ сразу вводится в поле — код перебирает частые селекторы (input[name="captcha"], input[name="code"] и т. п.).

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

GeeTest v3 отдаёт параметры gt и challenge в сетевом ответе, а не в HTML. Playwright умеет слушать трафик через событие response, поэтому нужные значения перехватываются на лету и складываются в объект для последующей отправки.

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;
}

Класс автоматизации входа целиком

Всё предыдущее собрано в класс PlaywrightAutomation: он поднимает браузер, заполняет форму, вызывает detectAndSolve, подставляет токен и отправляет форму. Метод loginWithCaptcha проходит сценарий входа от начала до конца — удобно для интеграционного тестирования QA и проверки формы в staging.

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://https://staging.example.com/qa-login",
    {
      "#email": "user@example.com",
      "#password": "pass123",
    },
    "#login-btn"
  );
  console.log(`Redirected to: ${result}`);
} finally {
  await bot.stop();
}

Диагностика типичных проблем

Большинство сбоев в этой связке — это гонки загрузки и пропущенные колбэки, а не ошибки самого решения. Таблица ниже собирает частые симптомы и быстрые исправления.

Симптом Причина Что делать
page.evaluate возвращает null Элемент ещё не загрузился Сначала вызвать waitForSelector
Cloudflare Turnstile не обнаружен Подгружается через JS после загрузки страницы Дождаться селектора .cf-turnstile
Форма не отправляется после инъекции токена Не сработал колбэк Явно вызвать callback reCAPTCHA
Автоматизацию распознаёт сайт Не выполнен инициализационный скрипт Добавить переопределение webdriver
Тайм-аут networkidle Скрипты с длинным опросом Использовать domcontentloaded

Частые вопросы

Сколько потоков CaptchaAI нужно, чтобы запускать несколько экземпляров Playwright параллельно?

Ровно столько, сколько одновременных задач CAPTCHA у вас в полёте: один поток = одна задача в моменте. Пять параллельных вкладок с капчей закрывает тариф BASIC ($15/мес, 5 потоков); для десятков воркеров берите ADVANCE ($90/мес, 50 потоков). Число решений в месяц не ограничено.

Почему инъекция токена reCAPTCHA не приводит к отправке формы?

Чаще всего форме мало значения в g-recaptcha-response — она ждёт вызова JS-колбэка виджета. Именно поэтому функция solveRecaptchaV2 дополнительно перебирает клиентов ___grecaptcha_cfg и вызывает их callback с токеном.

Одинаково ли надёжно решение в headless-режиме и с видимым окном?

Да. Поставьте headless: true в launch() — CaptchaAI решает задачу на своей стороне, поэтому режим окна не влияет на результат. Видимое окно удобно только для отладки селекторов.

Можно ли переиспользовать этот код для reCAPTCHA v3 и GeeTest v3?

Да. reCAPTCHA v3 отправляется тем же методом userrecaptcha (с параметрами action и min_score), а GeeTest v3 — методом geetest с перехваченными gt и challenge. Меняются только параметры, а цикл submit → опрос → инъекция остаётся тем же.


Итог

Node.js + Playwright + CaptchaAI дают современный стек автоматизации. Ключевые опорные точки этого руководства:

  • единый цикл submit → опрос → инъекция для всех типов капчи;
  • автоопределение reCAPTCHA, Turnstile и image-капчи по разметке;
  • перехват сетевых ответов для параметров GeeTest v3;
  • класс PlaywrightAutomation, закрывающий вход целиком — от формы до отправки токена.

Похожие статьи

Комментарии для этой статьи отключены.