Use Cases

Парсинг CAPTCHA с помощью Node.js: полное руководство

Если парсер на Node.js упирается в reCAPTCHA или Cloudflare Turnstile, поднимать headless-браузер не обязательно: CAPTCHA можно решать отдельным сервисом параллельно с обычными HTTP-запросами. Ниже — рабочая связка axios + Cheerio + API CaptchaAI: без Puppeteer там, где сайт отдаёт стандартный HTML-ответ. Перед запуском в проде решите заранее, какие данные вы вправе собирать, — это особенно актуально при работе с персональными данными по 152-ФЗ или GDPR.

Такой подход экономит и CPU, и время: headless-браузер тратит ресурсы на рендеринг DOM и выполнение скриптов, которые парсеру не нужны, если сайт в итоге отдаёт обычный HTML-ответ. axios делает лёгкий HTTP-запрос, а CaptchaAI решает CAPTCHA как отдельный, независимый шаг конвейера.

Что понадобится

Требование Подробности
Node.js 16+ c npm
Axios npm install axios
Cheerio npm install cheerio
API-ключ CaptchaAI С captchaai.com

Ключ выдаётся сразу после регистрации в личном кабинете и действует для всех типов CAPTCHA, которые поддерживает CaptchaAI, — отдельный ключ на каждый тип не нужен.

Модуль для решения CAPTCHA через API CaptchaAI

Вынесите отправку и опрос задач в отдельный класс — он пригодится и для reCAPTCHA v2/v3, и для Cloudflare Turnstile:

// captcha-solver.js
const axios = require("axios");

class CaptchaSolver {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.baseUrl = "https://ocr.captchaai.com";
  }

  async _submit(params) {
    params.key = this.apiKey;
    const resp = await axios.get(`${this.baseUrl}/in.php`, { params });
    if (!resp.data.startsWith("OK|")) {
      throw new Error(`Submit error: ${resp.data}`);
    }
    return resp.data.split("|")[1];
  }

  async _poll(taskId, timeout = 300000) {
    const deadline = Date.now() + timeout;
    while (Date.now() < deadline) {
      await new Promise((r) => setTimeout(r, 5000));
      const resp = await axios.get(`${this.baseUrl}/res.php`, {
        params: { key: this.apiKey, action: "get", id: taskId },
      });
      if (resp.data === "CAPCHA_NOT_READY") continue;
      if (resp.data.startsWith("OK|")) return resp.data.split("|")[1];
      throw new Error(`Solve error: ${resp.data}`);
    }
    throw new Error("Solve timed out");
  }

  async solveRecaptchaV2(siteKey, pageUrl) {
    const taskId = await this._submit({
      method: "userrecaptcha",
      googlekey: siteKey,
      pageurl: pageUrl,
    });
    return this._poll(taskId);
  }

  async solveRecaptchaV3(siteKey, pageUrl, action = "verify") {
    const taskId = await this._submit({
      method: "userrecaptcha",
      googlekey: siteKey,
      pageurl: pageUrl,
      version: "v3",
      action,
    });
    return this._poll(taskId);
  }

  async solveTurnstile(siteKey, pageUrl) {
    const taskId = await this._submit({
      method: "turnstile",
      sitekey: siteKey,
      pageurl: pageUrl,
    });
    return this._poll(taskId);
  }
}

module.exports = CaptchaSolver;

Класс намеренно не привязан к конкретному сценарию парсинга: _submit и _poll инкапсулируют опрос in.php/res.php, а публичные методы solveRecaptchaV2, solveRecaptchaV3 и solveTurnstile просто передают в них нужные параметры. Это удобно, когда в одном проекте нужно решать разные типы CAPTCHA на разных сайтах — модуль подключается один раз и переиспользуется без копирования логики опроса.

Парсинг страницы, защищённой reCAPTCHA

Логика простая: загрузить страницу, достать sitekey, получить токен у CaptchaAI и отправить форму заново — уже с решённой CAPTCHA. На reCAPTCHA v2 решение обычно укладывается менее чем в 60 секунд — закладывайте это время в таймаут запроса, чтобы парсер не обрывал соединение раньше срока:

const axios = require("axios");
const cheerio = require("cheerio");
const CaptchaSolver = require("./captcha-solver");

const solver = new CaptchaSolver("YOUR_API_KEY");

async function scrapeProtectedPage(url) {
  // Step 1: Load the page
  const { data: html } = await axios.get(url, {
    headers: {
      "User-Agent":
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    },
  });

  const $ = cheerio.load(html);

  // Step 2: Extract site key
  const siteKey = $(".g-recaptcha").attr("data-sitekey");
  if (!siteKey) {
    console.log("No CAPTCHA found, page loaded directly");
    return html;
  }

  console.log("Site key found:", siteKey);

  // Step 3: Solve the CAPTCHA
  const token = await solver.solveRecaptchaV2(siteKey, url);
  console.log("Token received:", token.substring(0, 50));

  // Step 4: Submit with the token
  const result = await axios.post(
    url,
    new URLSearchParams({
      "g-recaptcha-response": token,
      q: "search query",
    }),
    {
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        "User-Agent":
          "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
      },
    }
  );

  return result.data;
}

Параллельный парсинг нескольких страниц

Для очереди URL достаточно пула воркеров: каждый забирает следующую страницу из очереди, решает CAPTCHA и делает запрос независимо от остальных. Такой пул хорошо ложится на модель тарификации CaptchaAI: платите не за количество решённых CAPTCHA, а за число одновременных потоков, поэтому выбор concurrency — это, по сути, прямой выбор тарифа.

async function scrapePages(urls, siteKey, concurrency = 3) {
  const results = [];
  const queue = [...urls];

  const worker = async () => {
    while (queue.length > 0) {
      const url = queue.shift();
      try {
        const token = await solver.solveRecaptchaV2(siteKey, url);
        const { data } = await axios.post(
          url,
          new URLSearchParams({ "g-recaptcha-response": token }),
          {
            headers: {
              "User-Agent":
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
            },
          }
        );
        results.push({ url, data, success: true });
        console.log(`Scraped: ${url}`);
      } catch (err) {
        results.push({ url, error: err.message, success: false });
        console.error(`Failed: ${url} - ${err.message}`);
      }
    }
  };

  // Run workers concurrently
  const workers = Array(concurrency)
    .fill(null)
    .map(() => worker());
  await Promise.all(workers);

  return results;
}

// Usage
const urls = [
  "https://example.com/page/1",
  "https://example.com/page/2",
  "https://example.com/page/3",
];
const results = await scrapePages(urls, "6Le-wvkS...", 3);

Значение concurrency напрямую упирается в тариф CaptchaAI: он тарифицируется по числу одновременных потоков, а не по количеству решённых CAPTCHA. Для трёх воркеров хватит BASIC ($15/мес, 5 потоков); для пары десятков — STANDARD ($30/мес, 15 потоков) или ADVANCE ($90/мес, 50 потоков).

Некоторым сайтам нужны сессионные cookie, установленные до отправки формы. Для этого используйте axios с сохранением cookie. Без общего jar каждый запрос axios стартует с чистого состояния, и сайт может воспринимать загрузку страницы и отправку формы как два разных визита — тогда решённая CAPTCHA не помогает, потому что сессия уже не совпадает:

const { wrapper } = require("axios-cookiejar-support");
const { CookieJar } = require("tough-cookie");

const jar = new CookieJar();
const client = wrapper(
  axios.create({
    jar,
    headers: {
      "User-Agent":
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    },
  })
);

async function scrapeWithSession(url, siteKey) {
  // Initial page load sets cookies
  await client.get(url);

  // Solve CAPTCHA
  const token = await solver.solveRecaptchaV2(siteKey, url);

  // Submit with maintained cookies
  const result = await client.post(
    url,
    new URLSearchParams({ "g-recaptcha-response": token })
  );

  return result.data;
}

Разбор результатов через Cheerio

После успешной отправки формы остаётся вытащить нужные поля из HTML-ответа:

function parseResults(html) {
  const $ = cheerio.load(html);
  const items = [];

  $(".result-item").each((_, el) => {
    items.push({
      title: $(el).find(".title").text().trim(),
      url: $(el).find("a").attr("href"),
      description: $(el).find(".description").text().trim(),
    });
  });

  return items;
}

Решение типичных проблем

Проблема Причина Решение
CAPTCHA_NOT_READY возвращается бесконечно Неверный sitekey или медленное решение на стороне провайдера Проверьте sitekey; увеличьте таймаут опроса
403 Forbidden на POST-запросе Не сохранены cookie или заголовки Используйте сессионные cookie; добавьте заголовок Referer
Cheerio не находит нужные элементы Контент рендерится динамически на JS Для таких страниц используйте Puppeteer вместо axios + Cheerio
ECONNREFUSED Целевой сайт ограничивает частоту запросов Добавьте задержки между запросами; используйте ротацию прокси

При ECONNREFUSED и похожих сетевых ошибках не повторяйте запрос сразу — добавьте экспоненциальную задержку между попытками и ограничьте число повторов. Это снижает нагрузку на целевой сайт и уменьшает вероятность, что парсер попадёт под более жёсткое ограничение частоты запросов.

Часто задаваемые вопросы

Когда вместо axios нужен Puppeteer?

Axios + Cheerio достаточно, если целевой сайт отвечает обычным HTML после отправки формы. Переходите на Puppeteer, когда контент требует выполнения JavaScript, есть динамический рендеринг или сложное взаимодействие с интерфейсом.

Сколько потоков CaptchaAI нужно для парсинга на Node.js?

Ориентируйтесь на concurrency в пуле воркеров: каждый одновременный запрос занимает один поток. Для 3–5 параллельных запросов хватает BASIC (5 потоков), для 15–50 — STANDARD или ADVANCE.

Как передать прокси в axios при парсинге с CAPTCHA?

Настройте proxy в конфиге axios-запроса и используйте один и тот же IP для загрузки страницы и для отправки формы с токеном — иначе сайт может отклонить сессию.

Как обрабатывать сайты с Cloudflare Turnstile?

Если сайт использует Turnstile, вызывайте solver.solveTurnstile(). Для полноценных Cloudflare-заданий (Challenge Page) используйте руководство по решению Cloudflare Challenge — оно возвращает cookie cf_clearance.

Сколько занимает решение CAPTCHA при парсинге?

Зависит от типа: Cloudflare Turnstile обычно решается менее чем за 10 секунд, reCAPTCHA v2 — менее чем за 60 секунд. Закладывайте эти ориентиры в таймауты HTTP-запросов и в логику опроса res.php, чтобы парсер не отваливался раньше времени.

Похожие руководства

Ниже — смежные темы: решение CAPTCHA в связке с полноценным headless-браузером, тот же подход на Python и работа с прокси при больших объёмах парсинга.

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