Когда 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
Одна функция закрывает весь цикл из трёх шагов:
POSTнаin.phpотправляет задачу и возвращает её ID;res.phpопрашивается раз в 5 секунд (до 30 попыток) до готового результата;- при
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, закрывающий вход целиком — от формы до отправки токена.