Grid-капчу нельзя «прочитать» как обычную картинку: сервису нужно отдать и скриншот сетки, и текст инструкции («выберите все квадраты со светофорами»), а в ответ придут номера плиток, по которым надо кликнуть. Ниже — полный цикл на Node.js: Puppeteer снимает сетку внутри iframe reCAPTCHA, axios отправляет её в CaptchaAI методом post, скрипт опрашивает res.php и кликает по возвращённым ячейкам. Код рабочий, менять нужно только API-ключ и URL страницы.
Задача всплывает обычно в двух местах. Первое — интеграционные тесты, где форма регистрации в staging закрыта reCAPTCHA v2 и прогон падает на каждом билде. Второе — парсинг публичных каталогов, где сетка появляется раз в несколько сотен запросов и молча ломает всю очередь.
Что понадобится до старта
Стек здесь минимальный, но два момента ломают сборку чаще всего:
- Пакет
form-dataставится отдельно — сетка уходит на сервер как multipart-файл, а не как base64-строка в JSON. - Puppeteer тянет за собой Chromium, поэтому в CI-образе нужны системные библиотеки; на голом
node:alpineзапуск упадёт ещё до первого скриншота.
Окружение
| Элемент | Значение |
|---|---|
| CaptchaAI API-ключ | Отcaptchaai.com |
| Node.js | 14+ |
| Библиотеки | axios, puppeteer, form-data |
Тарификация у CaptchaAI идёт по потокам, а не по количеству решений. BASIC ($15/мес, 5 потоков) держит пять одновременных задач, ADVANCE ($90/мес, 50 потоков) закрывает параллельный прогон тестов на нескольких воркерах. Поток занят ровно столько, сколько решается текущая сетка; после ответа он сразу свободен под следующую задачу. Для CI-раннера, который гоняет пару десятков сценариев за ночь, пяти потоков обычно достаточно — и для команды, считающей бюджет в валюте с плавающим курсом, предсказуемая месячная сумма в USD удобнее почасовой арифметики.
Grid-капча входит в число поддерживаемых типов наравне с reCAPTCHA v2 и v3, Cloudflare Turnstile, GeeTest v3 и обычным OCR. hCaptcha и FunCaptcha сервис не решает, GeeTest v4 заявлен как «скоро» — если ваш сценарий упирается в них, дальше по этому руководству идти нет смысла.
Шаг 1. Снимите скриншот сетки
Сетка живёт не в основном документе, а во вложенном iframe с адресом recaptcha/api2/bframe. Поэтому сначала находим нужный фрейм среди page.frames(), вытаскиваем из него текст инструкции и только потом снимаем скриншот контейнера .rc-imageselect-target — именно его, а не всю страницу: иначе в кадр попадут посторонние элементы и качество распознавания упадёт.
const puppeteer = require('puppeteer');
const fs = require('fs');
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com/page-with-recaptcha');
// Switch to the reCAPTCHA challenge iframe
const frames = page.frames();
const challengeFrame = frames.find((f) => f.url().includes('recaptcha/api2/bframe'));
// Get the instruction text
const instruction = await challengeFrame.$eval(
'.rc-imageselect-desc-no-canonical',
(el) => el.textContent.trim()
);
// Screenshot the grid
const grid = await challengeFrame.$('.rc-imageselect-target');
await grid.screenshot({ path: 'grid.png' });
Если скриншот получился пустым
- Фрейм ещё не отрисован — дождитесь
.rc-imageselect-targetчерезwaitForSelector, а не фиксированной паузы. - Выбран не тот iframe: у виджета их два,
anchor(чекбокс) иbframe(сам челлендж); сетка только во втором. $evalвернул пустую инструкцию — значит вёрстка челленджа отличается, проверьте селектор.rc-imageselect-desc-no-canonicalв реальном DOM: у части вариантов класс другой.
Инструкция важна не меньше картинки: без неё сервис не знает, что искать в кадре.
Шаг 2. Отправьте задачу в CaptchaAI
Задача уходит на in.php обычным multipart-POST. Порядок полей значения не имеет, но четыре из них обязательны:
method=post— признак того, что вы загружаете файл, а не передаёте sitekey.img_type=recaptcha— говорит сервису, что это сетка reCAPTCHA, а не произвольная картинка.grid_size— размерность (3x3или4x4).instructions— та самая строка из шага 1.
Флаг json=1 заставляет API отвечать структурированным JSON вместо «голого» текста — с ним разбор ошибок заметно проще.
const axios = require('axios');
const FormData = require('form-data');
const API_KEY = 'YOUR_API_KEY';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const form = new FormData();
form.append('key', API_KEY);
form.append('method', 'post');
form.append('grid_size', '3x3');
form.append('img_type', 'recaptcha');
form.append('instructions', instruction);
form.append('json', '1');
form.append('file', fs.createReadStream('grid.png'));
const { data: submitData } = await axios.post('https://ocr.captchaai.com/in.php', form, {
headers: form.getHeaders(),
});
if (submitData.status !== 1) throw new Error(submitData.request);
const taskId = submitData.request;
console.log(`Task submitted: ${taskId}`);
При status !== 1 в поле request лежит код ошибки, а не ID задачи. Логируйте это поле целиком, иначе на проде вы получите бесполезное «решение не пришло» без причины.
Значение в request |
Что это значит на практике |
|---|---|
ERROR_WRONG_USER_KEY |
Опечатка в ключе или лишние пробелы при чтении из переменной окружения |
ERROR_ZERO_BALANCE |
Баланс исчерпан — пополните аккаунт в панели управления |
ERROR_NO_SLOT_AVAILABLE |
Все потоки тарифа заняты; уменьшите параллелизм или перейдите на тариф выше |
CAPCHA_NOT_READY |
Не ошибка: решение ещё считается, продолжайте опрос |
Шаг 3. Опросите res.php до готового ответа
CaptchaAI работает асинхронно: сначала вы получаете ID задачи, затем опрашиваете res.php, пока ответ не будет готов. Первую паузу делаем в пять секунд и дальше опрашиваем с тем же интервалом. Пока решение не готово, API отдаёт CAPCHA_NOT_READY — это нормальное состояние, а не ошибка; любое другое значение нужно бросать как исключение и не тратить остаток цикла впустую.
await sleep(5000);
let cellsToClick;
for (let i = 0; i < 30; i++) {
const { data: pollData } = await axios.get('https://ocr.captchaai.com/res.php', {
params: { key: API_KEY, action: 'get', id: taskId, json: 1 },
});
if (pollData.status === 1) {
cellsToClick = JSON.parse(pollData.request);
console.log('Click cells:', cellsToClick);
break;
}
if (pollData.request !== 'CAPCHA_NOT_READY') {
throw new Error(pollData.request);
}
await sleep(5000);
}
Тридцать итераций по пять секунд дают потолок примерно в две с половиной минуты — с запасом. Опрашивать чаще раза в секунду смысла нет: решение от этого не ускоряется, а к ограничению частоты запросов вы приближаетесь. На нестабильном мобильном канале или при деплое в европейском регионе увеличивайте не частоту опроса, а общий тайм-аут.
Шаг 4. Кликните по нужным плиткам
В ответе приходит массив номеров ячеек, пронумерованных с единицы слева направо и сверху вниз. Массив DOM-элементов индексируется с нуля — отсюда cellNum - 1. Задержка в 300 мс между кликами нужна потому, что виджет reCAPTCHA проигрывает анимацию выделения и слишком быстрые клики попросту не регистрирует.
const tiles = await challengeFrame.$$('.rc-imageselect-tile');
for (const cellNum of cellsToClick) {
await tiles[cellNum - 1].click();
await sleep(300);
}
// Click verify
await challengeFrame.click('#recaptcha-verify-button');
console.log(`Solved: clicked tiles ${JSON.stringify(cellsToClick)}`);
await browser.close();
Ожидаемый результат:
Click cells: [1, 3, 6, 9]
Solved: clicked tiles [1,3,6,9]
Что происходит после проверки
После нажатия #recaptcha-verify-button виджет либо закрывается и отдаёт токен в поле g-recaptcha-response, либо подгружает новый набор плиток — второй случай разбираем в FAQ ниже.
Как встроить это в рабочий пайплайн
Одиночный скрипт из четырёх шагов удобен для проверки ключа, но в реальном проекте стоит учесть ещё несколько вещей.
- Оберните шаги 2–4 в одну функцию с повтором. Одна неудачная сетка — не повод ронять весь прогон; двух-трёх повторных попыток с экспоненциальной задержкой обычно хватает.
- Ограничьте параллелизм числом потоков вашего тарифа. Пять воркеров на BASIC — ровно по границе; шестой получит отказ, а не ускорение.
- Сохраняйте
grid.pngи текст инструкции при неуспехе. Это единственный способ потом понять, где была проблема: в кадре, в инструкции или в самом решении. - Собирайте только те данные, которые вы вправе обрабатывать. Для команд, работающих с российскими пользователями, это прямая отсылка к 152-ФЗ «О персональных данных», для трансграничных проектов — привычная GDPR-дисциплина. Сама капча тут ни при чём, а вот логи со скриншотами чужих страниц — вполне.
Часто задаваемые вопросы
Чем grid-капча отличается от обычной image-капчи в API?
Обычная картинка отправляется без контекста, и в ответ приходит текст. Для сетки нужны ещё grid_size, img_type и instructions, а ответом будет массив номеров ячеек. Метод в обоих случаях один и тот же — post.
Сколько потоков брать под ночной прогон тестов?
Считайте по пиковому параллелизму, а не по общему числу задач: сколько браузеров работает одновременно, столько потоков и нужно. Для типового CI-раннера с 3–5 параллельными сценариями достаточно BASIC ($15/мес, 5 потоков), для нескольких очередей парсинга ближе STANDARD ($30/мес, 15 потоков).
Работает ли это с сетками 4x4?
Да. Установите для grid_size значение 4x4 в параметрах запроса. Скриншот и логика кликов не меняются — растёт только длина массива с номерами плиток.
Что делать, если после клика подгружается новый набор плиток?
Некоторые задачи reCAPTCHA обновляют часть изображений после проверки. Запустите цикл заново: снимите свежий скриншот, отправьте новую задачу и кликните по новому ответу. Заложите на такие раунды отдельный лимит попыток, иначе скрипт зациклится.
Можно ли использовать Playwright вместо Puppeteer?
Да. Вызовы API CaptchaAI не меняются вообще — отличается только код автоматизации браузера: поиск фрейма, снятие скриншота и клик по элементу.