API Tutorials

Решите CAPTCHA с изображением сетки с помощью Node.js и CaptchaAI

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. Порядок полей значения не имеет, но четыре из них обязательны:

  1. method=post — признак того, что вы загружаете файл, а не передаёте sitekey.
  2. img_type=recaptcha — говорит сервису, что это сетка reCAPTCHA, а не произвольная картинка.
  3. grid_size — размерность (3x3 или 4x4).
  4. 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 не меняются вообще — отличается только код автоматизации браузера: поиск фрейма, снятие скриншота и клик по элементу.


Связанные руководства


Начните решать Grid Image CAPTCHA с помощью CaptchaAI →

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