Если парсер на 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 и сессиями
Некоторым сайтам нужны сессионные 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 и работа с прокси при больших объёмах парсинга.