Как обрабатывать капчу в Crawlee с помощью Node.js SDK

В Crawlee нет хука для капчи, и он не нужен. Капча в Crawlee решается прямо внутри вашего requestHandler, посреди запроса, для которого у вас уже есть страница браузера. Работать это заставляют три вещи: обнаруживайте виджет до того, как потратите на него решение, увеличьте таймаут обработчика, потому что значение по умолчанию меньше времени решения reCAPTCHA, и бросайте исключение при сбое, чтобы Crawlee повторял запрос через свою очередь, а не через ваш цикл. В этом руководстве показаны все три приёма на примере PlaywrightCrawler.
Что понадобится
- Node.js 18 или новее и проект Crawlee, который уже что-то обходит
- Запущенный и доступный CapSkip. Local mode слушает 127.0.0.1 на порту 8080 для автоматизации на той же машине, а Server mode слушает ваш сетевой или публичный IP, чтобы к нему мог обратиться краулер на другой машине, VPS или контейнерном хосте. Оба режима описаны в разделе Настройки подключения
- Три пакета, устанавливаемые вместе
# One install for the crawler, the browser and the solver client. npm install crawlee playwright capskip # Crawlee drives a real browser, so fetch one. npx playwright install chromium
Примеры здесь на CommonJS, именно в таком виде они описаны в README CapSkip. Crawlee 3 поставляется в обеих сборках, поэтому в ESM-проекте для краулера можно использовать import.
Где выполняется решение: внутри requestHandler
В Scrapy есть downloader middleware, а в Selenium есть та обёртка, которую вы написали сами. Crawlee даёт вам объект страницы напрямую, поэтому писать слой перехвата не нужно. Вы обнаруживаете проверку, решаете её и продолжаете работу в той же функции.
Сначала обнаружение. Запуск решения на каждой странице тратит ресурсы на страницы, где проверки вообще не было, и скрывает полезный сигнал о том, как часто вас на самом деле блокируют.
// npm install crawlee playwright capskip
const { PlaywrightCrawler } = require('crawlee');
const { CapSkip } = require('capskip');
// Local mode. Point host at a server IP to share one solver.
const solver = new CapSkip({ host: '127.0.0.1', port: 8080 });
async function solveIfChallenged(page, url, log) {
const widget = page.locator('[data-sitekey]').first();
if ((await widget.count()) === 0) return false;
const sitekey = await widget.getAttribute('data-sitekey');
log.info(`Solving sitekey ${sitekey}`);
const result = await solver.recaptcha(sitekey, url);
return result.code; // the token
}Атрибут data-sitekey задаётся на div виджета для reCAPTCHA v2, а также на div Turnstile, поэтому один селектор покрывает оба варианта. У reCAPTCHA v3 видимого виджета нет, поэтому ключ вы читаете из URL скрипта.
Вставьте токен, затем отправьте форму
Решение даёт вам токен. Страница по-прежнему ждёт этот токен в скрытом поле, которое заполнил бы её собственный виджет, поэтому поместите его туда и отправьте форму так, как это сделал бы браузер.
// The widget writes into a hidden textarea. Do the same.
await page.evaluate((token) => {
const field = document.getElementById('g-recaptcha-response');
field.value = token;
}, token);
// Then submit exactly as the page would, and wait for the result.
await Promise.all([
page.waitForNavigation(),
page.click('button[type=submit]'),
]);Некоторые страницы вместо отправки формы вызывают JavaScript-колбэк. Если у div виджета есть атрибут data-callback , вызывайте эту функцию с токеном, а не кликайте по чему-либо, потому что обработчик клика может вообще не сработать.
Первым делом увеличьте requestHandlerTimeoutSecs
Именно на этом чаще всего попадаются, и выглядит это как проблема решателя, хотя дело не в нём.
PlaywrightCrawler даёт каждому обработчику запросов 60 секунд по умолчанию. Задача reCAPTCHA v2 не готова первые 15-20 секунд, v3 занимает 10-15, и это ещё до того, как вы загрузили страницу, вставили токен и дождались навигации. Обработчик убивают посреди решения, Crawlee пишет в лог таймаут, и запрос возвращается в очередь, чтобы всё началось заново.
// npm install crawlee playwright capskip
const crawler = new PlaywrightCrawler({
// 60 is the default and it is shorter than a v2 solve plus a submit.
requestHandlerTimeoutSecs: 180,
// Three tries per URL, which is Crawlee's default and the right one.
maxRequestRetries: 3,
async requestHandler({ page, request, log }) {
// your handler
},
});Разумный потолок составляет 180 секунд. Это примерно в десять раз больше обычного времени решения, и он намеренно ниже собственного лимита опроса SDK в 300 секунд для reCAPTCHA, поэтому по-настоящему зависший запрос отбрасывает Crawlee, а не держит слот браузера целых пять минут. Если вы предпочитаете, чтобы первым сдавался клиент решателя, снизьте recaptchaTimeout до значения меньше вашего таймаута обработчика.
Полный рабочий пример
Один файл, один краулер, один путь решения. Подставьте свой стартовый URL в вызов run.
// npm install crawlee playwright capskip
const { PlaywrightCrawler, Dataset } = require('crawlee');
const { CapSkip } = require('capskip');
const solver = new CapSkip({ host: '127.0.0.1', port: 8080 });
const crawler = new PlaywrightCrawler({
requestHandlerTimeoutSecs: 180,
maxRequestRetries: 3,
async requestHandler({ page, request, log }) {
const widget = page.locator('[data-sitekey]').first();
if ((await widget.count()) > 0) {
const sitekey = await widget.getAttribute('data-sitekey');
const result = await solver.recaptcha(sitekey, request.loadedUrl);
await page.evaluate((token) => {
document.getElementById('g-recaptcha-response').value = token;
}, result.code);
await Promise.all([
page.waitForNavigation(),
page.click('button[type=submit]'),
]);
log.info(`Cleared the challenge on ${request.loadedUrl}`);
}
await Dataset.pushData({ url: request.loadedUrl, title: await page.title() });
},
});
await crawler.run(['https://example.com/page-with-recaptcha']);Решение выполняется на вашей собственной машине, поэтому бюджет повторов выше не стоит ничего, кроме реального времени. В этом и состоит практическая разница с тарифицируемым сервисом, где три попытки на URL превращаются в строку счёта.
Пусть повторами занимается очередь, не пишите свой цикл
Первым делом хочется обернуть решение в цикл for. Не делайте этого. В Crawlee уже есть система повторов, которая знает про очередь запросов, пул сессий и конфигурацию прокси, а самописный цикл внутри обработчика невидим для всех трёх.
Вместо этого бросьте исключение. Обработчик, который бросает исключение, возвращает запрос в очередь, и Crawlee повторяет его до maxRequestRetries раз с новым контекстом браузера.
// npm install capskip
const { ApiException, NetworkException, TimeoutException } = require('capskip');
const crawler = new PlaywrightCrawler({
requestHandlerTimeoutSecs: 180,
// Runs between retries, while attempts remain.
errorHandler({ request, log }, error) {
log.warning(`Retry ${request.retryCount} for ${request.url}: ${error.message}`);
},
// Runs once, after the last attempt fails.
failedRequestHandler({ request, log }) {
log.error(`Gave up on ${request.url}`);
},
});Какое именно исключение выпало, подсказывает, что менять. NetworkException означает, что CapSkip недоступен, поэтому проверьте хост и порт, прежде чем винить сайт. TimeoutException означает, что окно опроса истекло и страница, скорее всего, отдаёт проверку сложнее, чем вы думаете. ApiException несёт возвращённый код ошибки, и именно его стоит логировать вместе с URL.
Краулер и решатель на разных машинах
Crawlee масштабируется за счёт запуска новых своих копий, а флот краулеров на отдельных машинах не может весь обращаться к 127.0.0.1. Здесь помогает Server mode: CapSkip слушает ваш сетевой или публичный IP вместо loopback, и все воркеры смотрят на один и тот же адрес.
// npm install capskip
const { CapSkip } = require('capskip');
// Same client, different address. Nothing else in the code changes.
const solver = new CapSkip({
host: process.env.CAPSKIP_HOST || '127.0.0.1',
port: Number(process.env.CAPSKIP_PORT || 8080),
});SDK сам читает CAPSKIP_HOST и CAPSKIP_PORT из окружения, поэтому запасной вариант выше нужен лишь как подстраховка для контейнера, который стартует без них. Для машины с решателем рекомендуется статический публичный IP, а шаги настройки описаны в разделе Настройки подключения. Это по-прежнему ваше железо и по-прежнему без тарификации, так что изменилось только то, где работает процесс.
Частые ошибки и что они означают
| Симптом | Причина | Исправить |
|---|---|---|
| requestHandler завершился по таймауту через 60 секунд | Таймаут обработчика по умолчанию короче, чем время решения | Установите requestHandlerTimeoutSecs в 180 |
| Решение проходит, но страница всё равно блокирует | Токен вставлен, но форма так и не была отправлена | Проверьте наличие атрибута data-callback и вызовите его |
ERROR_GOOGLEKEY | Атрибут sitekey оказался пустым или прочитан не с того элемента | Логируйте значение перед решением; ключи v3 находятся в URL скрипта |
ERROR_PAGEURL | Обработчик передал относительный или перенаправленный URL | Используйте request.loadedUrl, который содержит URL после редиректов |
| NetworkException на каждом запросе | Краулер не может достучаться до решателя | Local mode работает только на loopback; для удалённого воркера переключитесь на Server mode |
| Каждый URL повторяется три раза, а затем отбрасывается | Обработчик бросает исключение до решения | Прочитайте лог errorHandler; настоящая причина в первом сбое |
Названия параметров и полный список кодов ошибок находятся в разделе Документация по API.
FAQ
Работает ли это с CheerioCrawler?
Частично. У CheerioCrawler нет браузера, поэтому нет ни объекта страницы, ни способа выполнить собственный JavaScript виджета. Вы всё ещё можете вытащить sitekey из HTML, решить капчу и сами отправить токен в теле формы. Этого хватит для обычной отправки формы и не хватит для всего, что ждёт колбэк. Когда проверка вероятна, используйте PlaywrightCrawler.
Может, лучше решать в preNavigationHook?
Нет. Хуки до навигации выполняются до загрузки страницы, поэтому обнаруживать ещё нечего. Хуки после навигации ближе, но именно в обработчике запросов у вас уже есть страница, URL после редиректов и логгер. Оставьте решение там, а хуки используйте для cookie и заголовков.
Может ли краулер работать на хостинговой платформе, пока решатель остаётся дома?
Да, если CapSkip работает в Server mode. Краулеру нужен маршрут до адреса решателя, поэтому домашнему подключению понадобятся статический публичный IP и открытый порт, а VPS будет более простым вариантом. Клиентский код в обоих случаях одинаковый: меняется только значение хоста.
Сколько параллельных решений может создать один обход?
Crawlee сам масштабирует свою параллельность, и каждый обработчик независимо ожидает своё решение, поэтому на клиенте нечего настраивать в плане очереди. SDK начинает опрос с 250 миллисекунд и постепенно увеличивает интервал до потолка pollingInterval, благодаря чему быстрое решение остаётся быстрым, даже когда в работе сразу несколько. Подбирайте параллельность Crawlee под то, что терпит целевой сайт, а не под решатель.
Коротко
Обнаружьте виджет, решайте в обработчике запросов, поднимите таймаут обработчика до 180 секунд и бросайте исключение, чтобы очередь повторила попытку. Именно то, что решатель работает у вас самих, делает три повтора разумным значением по умолчанию, а не вопросом расходов, и это тот же аргумент за то, чтобы у вас был локальный обход капчи в любом месте обхода. Руководство по интеграции с Node.js описывает настройку клиента, руководство по Playwright содержит детали на стороне браузера, которые наследует Crawlee, а решение капчи для парсинга описывает работу с сессиями в рамках всего обхода. Тот же приём на Python смотрите в статье о middleware для Scrapy.
