Crawlee исправно тянет прокси, повторы запросов и очередь, но CAPTCHA он не решает — это отдельная задача, которую нужно закрыть самостоятельно. Ниже — три рабочих сценария подключения CaptchaAI к Crawlee: обычный CheerioCrawler, PlaywrightCrawler для JS-страниц и пул сессий, который переживает решённую CAPTCHA между запросами. Код можно вставить в существующего паука без переписывания архитектуры.
Зачем подключать CaptchaAI к Crawlee
| Возможность Crawlee | Что это даёт вместе с CaptchaAI |
|---|---|
SessionPool (пул сессий) |
Одни и те же cookie и заголовки для всех запросов после решения CAPTCHA |
| Автоматический повтор запросов | Запрос уходит на повтор сразу после того, как CaptchaAI вернул токен |
| Ротация прокси | Работает вместе с прокси на стороне CaptchaAI без конфликтов настроек |
RequestQueue (очередь запросов) |
Решение CAPTCHA становится обычным шагом очереди, а не отдельным скриптом сбоку |
Каждая из этих возможностей уже встроена в Crawlee — добавить нужно только вызов CaptchaAI в нужной точке обработчика запроса. Ниже разобраны три такие точки: requestHandler обычного краулера, requestHandler браузерного краулера и слушатель пула сессий.
Шаг 1: базовая интеграция в CheerioCrawler
Самый простой случай — статическая страница с формой, где reCAPTCHA v2 подключена через data-sitekey. Обработчик проверяет наличие виджета, отправляет sitekey и адрес страницы в CaptchaAI, дожидается токена и подставляет его в форму:
const { CheerioCrawler } = require('crawlee');
const https = require('https');
const API_KEY = process.env.CAPTCHAAI_API_KEY;
async function solveCaptcha(sitekey, pageurl) {
// Submit task
const submitData = new URLSearchParams({
key: API_KEY,
method: 'userrecaptcha',
googlekey: sitekey,
pageurl: pageurl,
json: '1',
});
const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
method: 'POST',
body: submitData,
});
const submitResult = await submitResp.json();
if (submitResult.status !== 1) {
throw new Error(`Submit error: ${submitResult.request}`);
}
const taskId = submitResult.request;
// Poll for result
await new Promise(r => setTimeout(r, 15000));
for (let i = 0; i < 24; i++) {
const pollResp = await fetch(
`https://ocr.captchaai.com/res.php?key=${API_KEY}&action=get&id=${taskId}&json=1`
);
const pollResult = await pollResp.json();
if (pollResult.status === 1) return pollResult.request;
if (pollResult.request !== 'CAPCHA_NOT_READY') {
throw new Error(`Solve error: ${pollResult.request}`);
}
await new Promise(r => setTimeout(r, 5000));
}
throw new Error('Solve timeout');
}
// Crawlee spider with CAPTCHA handling
const crawler = new CheerioCrawler({
maxConcurrency: 5,
requestHandlerTimeoutSecs: 180,
async requestHandler({ request, $, log }) {
// Check if page has CAPTCHA
const captchaDiv = $('[data-sitekey]');
if (captchaDiv.length > 0) {
const sitekey = captchaDiv.attr('data-sitekey');
log.info(`CAPTCHA found on ${request.url}, solving...`);
const token = await solveCaptcha(sitekey, request.url);
log.info('CAPTCHA solved, submitting form');
// Submit form with token
const formData = new URLSearchParams({
'g-recaptcha-response': token,
});
const resp = await fetch(request.url, {
method: 'POST',
body: formData,
});
const html = await resp.text();
// Parse the result page...
}
// Extract data
const title = $('title').text();
const data = $('table tr').map((i, row) => ({
col1: $(row).find('td:eq(0)').text().trim(),
col2: $(row).find('td:eq(1)').text().trim(),
})).get();
log.info(`Scraped ${data.length} rows from ${request.url}`);
},
failedRequestHandler({ request, log }) {
log.error(`Failed: ${request.url}`);
},
});
// Run
(async () => {
await crawler.run([
'https://example.com/page1',
'https://example.com/page2',
]);
})();
Опрос res.php в примере выше ждёт 15 секунд перед первым запросом и повторяет ещё до 24 раз с интервалом 5 секунд — итого до 135 секунд ожидания. На практике reCAPTCHA v2 у CaptchaAI обычно решается быстрее: SLA-потолок — менее 60 секунд, так что запас в коде оставлен намеренно, под пиковую нагрузку.
Шаг 2: PlaywrightCrawler для JS-страниц с CAPTCHA
Если виджет reCAPTCHA появляется скриптом уже после рендера — например, через несколько секунд после DOMContentLoaded, — CheerioCrawler его просто не увидит: он не выполняет JavaScript. Здесь нужен PlaywrightCrawler:
const { PlaywrightCrawler } = require('crawlee');
const crawler = new PlaywrightCrawler({
maxConcurrency: 3,
requestHandlerTimeoutSecs: 180,
launchContext: {
launchOptions: {
headless: true,
args: ['--disable-blink-features=AutomationControlled'],
},
},
async requestHandler({ request, page, log }) {
await page.goto(request.url, { waitUntil: 'networkidle' });
// Check for reCAPTCHA
const sitekey = await page.evaluate(() => {
const el = document.querySelector('[data-sitekey]');
return el ? el.getAttribute('data-sitekey') : null;
});
if (sitekey) {
log.info(`CAPTCHA detected, solving for ${request.url}`);
const token = await solveCaptcha(sitekey, request.url);
// Inject token
await page.evaluate((t) => {
const ta = document.querySelector('[name="g-recaptcha-response"]');
if (ta) {
ta.style.display = 'block';
ta.value = t;
}
// Trigger callback
const widget = document.querySelector('.g-recaptcha');
if (widget) {
const cb = widget.getAttribute('data-callback');
if (cb && typeof window[cb] === 'function') {
window[cb](t);
}
}
}, token);
await page.click('button[type="submit"]');
await page.waitForNavigation({ waitUntil: 'networkidle' });
}
// Extract data
const title = await page.title();
const content = await page.textContent('body');
log.info(`Page: ${title}, length: ${content.length}`);
},
});
После решения токен подставляется в скрытое поле g-recaptcha-response и, если у виджета задан data-callback, дополнительно вызывается сам callback — иначе форма может не понять, что CAPTCHA пройдена, и заблокировать отправку.
Шаг 3: решение CAPTCHA с учётом пула сессий
Если сайт показывает CAPTCHA не всем запросам подряд, а только части сессий — например, после нескольких обращений с одного IP, — есть смысл хранить результат решения не в глобальной переменной, а в session.userData конкретной сессии Crawlee. Тогда повторное решение не потребуется, пока сессия остаётся рабочей:
const { CheerioCrawler, Session } = require('crawlee');
const crawler = new CheerioCrawler({
useSessionPool: true,
sessionPoolOptions: {
maxPoolSize: 10,
sessionOptions: {
maxUsageCount: 50,
},
},
async requestHandler({ request, $, session, log }) {
// If blocked, solve CAPTCHA and mark session as usable
if ($('.captcha-container').length > 0) {
const sitekey = $('[data-sitekey]').attr('data-sitekey');
const token = await solveCaptcha(sitekey, request.url);
// Store token in session for subsequent requests
session.userData = session.userData || {};
session.userData.captchaToken = token;
session.userData.tokenTime = Date.now();
log.info('CAPTCHA solved, session updated');
}
// Normal scraping
const items = $('div.item').map((i, el) => ({
name: $(el).find('.name').text().trim(),
price: $(el).find('.price').text().trim(),
})).get();
log.info(`Found ${items.length} items`);
},
});
useSessionPool в этом примере держит до 10 сессий с лимитом в 50 запросов каждая — подберите оба числа под реальный порог блокировки конкретного сайта, а не оставляйте значения по умолчанию.
Практический пример: мониторинг цен на маркетплейсе
Частый сценарий для Crawlee — регулярный обход карточек товаров на маркетплейсе, где часть страниц закрывается reCAPTCHA v2 после нескольких запросов подряд с одного прокси. Комбинация из ротации прокси, SessionPool и обработчика CaptchaAI из шага 3 закрывает это без ручного вмешательства: краулер продолжает обход, решая CAPTCHA только на тех сессиях, которым она действительно показана.
Для команд, которые разворачивают Crawlee в европейских дата-центрах или ближе к Казахстану и Центральной Азии, стоит заложить запас по requestHandlerTimeoutSecs — нестабильные сети партнёров увеличивают задержку между решением CAPTCHA и подтверждением формы. И отдельно: собирайте с карточек товара только те данные, которые нужны для мониторинга цен, — если в парсинг попадают персональные данные пользователей (отзывы, имена), для аудитории из РФ это уже вопрос 152-ФЗ «О персональных данных», а не только вежливости к чужому серверу.
Частые ошибки при интеграции Crawlee и CaptchaAI
- Слишком короткий
requestHandlerTimeoutSecs. Опросres.phpможет занять больше минуты; если тайм-аут обработчика меньше времени решения CAPTCHA, Crawlee пометит запрос как проваленный ещё до того, как CaptchaAI успеет ответить. - Общий селектор
sitekeyна несколько виджетов. Если на странице несколько форм с CAPTCHA,$('[data-sitekey]')может найти не тот элемент — ищитеdata-sitekeyвнутри контейнера конкретной формы, а не по всему документу. - Заголовок запроса не выставлен вручную.
URLSearchParamsв примерах выше сам расставляетapplication/x-www-form-urlencoded, но если отправку токена переносите на другой HTTP-клиент, заголовок нужно проставить явно — иначеin.phpвернёт ошибку разбора параметров.
Частые вопросы
Обрабатывает ли Crawlee CAPTCHA сам, без сторонних сервисов?
Нет. Crawlee отвечает за сессии, прокси и повторы запросов, а решение CAPTCHA нужно подключать отдельно — например, через CaptchaAI.
Нужно ли отключать ротацию прокси Crawlee при подключении CaptchaAI?
Нет, это независимые механизмы: прокси Crawlee меняет исходящий IP, а CaptchaAI решает саму CAPTCHA по sitekey и pageurl. Они не конфликтуют и обычно используются вместе, особенно при высоком maxConcurrency.
Сколько потоков CaptchaAI нужно для очереди Crawlee?
Зависит от maxConcurrency краулера и доли страниц с CAPTCHA. При пяти параллельных сессиях, где CAPTCHA встречается не на каждой странице, обычно хватает плана BASIC ($15/мес, 5 потоков); при maxConcurrency: 50 и частых CAPTCHA присмотритесь к ADVANCE ($90/мес, 50 потоков) — CaptchaAI тарифицирует по одновременным потокам, а не по числу решённых CAPTCHA.
Можно ли развернуть Crawlee с CaptchaAI в Apify?
Да. CaptchaAI вызывается по HTTP из actor'а Crawlee на Apify так же, как из любого Node.js-процесса — ключ API достаточно передать через переменную окружения Apify, менять код интеграции не нужно.
Что делать, если res.php долго возвращает CAPCHA_NOT_READY?
Это штатный статус, пока CaptchaAI ещё решает задачу, — ошибкой он становится, только если цикл опроса исчерпан. Если CAPCHA_NOT_READY держится дольше SLA-потолка reCAPTCHA v2 (около 60 секунд), проверьте, не устарел ли sitekey, и совпадает ли pageurl в запросе с реальным адресом страницы.
Похожие материалы
- Middleware для Scrapy Spider на базе CaptchaAI
- Как собрать собственную платформу парсинга с CaptchaAI
Подключите решение CAPTCHA к своему Crawlee-конвейеру — оформите API-ключ CaptchaAI.