Как решить капчу в Crawl4AI и не потерять сессию

Своего решателя капчи в Crawl4AI нет, поэтому решение капчи в Crawl4AI складывается из трёх вызовов, которые вы связываете сами: загрузить страницу в именованной сессии, решить капчу по sitekey внешним решателем, а затем вернуться в ту же вкладку, чтобы вставить токен и отправить форму. Спотыкаются обычно о порядок, в котором Crawl4AI выполняет действия. Начиная с версии 0.8.5, js_code выполняется после wait_for, поэтому обход, который отправляет форму в js_code и ждёт следующую страницу через wait_for, стоит на месте, пока не истечёт таймаут. Отправке место в js_code_before_wait. В этом руководстве разобран сценарий с сессией на примере reCAPTCHA v2, а затем хук для глубоких обходов, где вы не управляете каждым вызовом.
Что понадобится
- Python 3.10 или новее и Crawl4AI 0.9 или новее. Всё здесь прочитано в исходниках 0.9.4 и проверено на них; порядок из шага 3 действует начиная с 0.8.5.
- Пакет CapSkip для Python, в котором есть AsyncCapSkip, по-настоящему асинхронный клиент, встающий в тот событийный цикл, на котором Crawl4AI уже работает.
- URL страницы с капчей и селектор для элемента, который появляется только после того, как форма прошла.
- CapSkip, запущенный на машине с Windows. Режим Local отвечает на 127.0.0.1, когда краулер работает на той же машине; режим Server слушает ваш сетевой или публичный IP, когда это не так. Оба режима описаны в разделе Настройки подключения.
# pip install crawl4ai pip install -U crawl4ai capskip # Downloads the browser Crawl4AI drives, once per machine. crawl4ai-setup
Шаг 1: загружаем страницу в сессии и считываем sitekey
Передайте первому вызову session_id. Тогда вкладка остаётся открытой после возврата из вызова, с нетронутыми cookie и виджетом, и токен, который вы решите позже, попадёт в ту страницу, которая его запросила. Без session_id Crawl4AI закрывает страницу, как только получает HTML.
# pip install crawl4ai
import re
from crawl4ai import CrawlerRunConfig
PAGE_URL = "https://example.com/signup"
SESSION = "signup"
# The widget element, in any attribute order, with or without other classes.
WIDGET = r'<[^>]*class="(?:[^"]*\s)?g-recaptcha(?:\s[^"]*)?"[^>]*>'
async def read_sitekey(crawler):
# session_id keeps this tab open for the next arun call.
config = CrawlerRunConfig(session_id=SESSION)
first = await crawler.arun(PAGE_URL, config=config)
# Do not stop on first.success: a CAPTCHA page can be
# flagged as blocked while its HTML is complete.
widget = re.search(WIDGET, first.html)
match = widget and re.search(r'data-sitekey="([^"]+)"', widget.group(0))
return match.group(1) if match else NoneЭтот комментарий стоит там не просто так. Crawl4AI 0.9 прогоняет антибот-проверку по каждому результату, и страница, которую он принимает за страницу блокировки, помечается как неудачная: success возвращается равным False, а error_message начинается с Blocked by anti-bot protection. Под это подходит любой HTML-ответ с кодом 403 или 503, а также 429. При других статусах ошибок подходит страница меньше 10 KB, если атрибут class её виджета в точности равен g-recaptcha. Отрисованный HTML по-прежнему лежит в first.html и по-прежнему содержит нужный вам sitekey, поэтому прочитайте его, прежде чем решать, что обход провалился.
Поле html содержит отрисованную страницу, поэтому виджет, построенный скриптом, в нём тоже есть. Если ключа нет на элементе, ищите параметр k= в src iframe виджета, а если виджет отрисовывается поздно, добавьте в первый вызов wait_for на этот iframe.
Шаг 2: решаем капчу через AsyncCapSkip
Crawl4AI построен на asyncio сверху донизу, поэтому используйте асинхронный клиент. Он представляет собой настоящую корутину, а не обёртку вокруг блокирующих вызовов, а значит, решение, которое занимает двадцать секунд, не замораживает все остальные обходы в том же событийном цикле.
# pip install capskip
from capskip import AsyncCapSkip
solver = AsyncCapSkip(host="127.0.0.1", port=8080)
async def solve(sitekey):
# Invisible v2 takes invisible=1, Enterprise takes enterprise=1.
result = await solver.recaptcha(sitekey=sitekey, url=PAGE_URL)
return result["code"] # the g-recaptcha-response valueПока это выполняется, в Crawl4AI ничего не ждёт, потому что решение происходит между двумя вызовами, а не внутри одного. Вкладка просто стоит открытой. А вот у токена часы идут: токен reCAPTCHA годен примерно две минуты после выдачи, поэтому сразу переходите к отправке. Подробнее об этом: срок действия токена reCAPTCHA.
Шаг 3: вставляем токен и отправляем форму в той же вкладке
Второй вызов повторно использует сессию и задаёт js_only: этот флаг велит Crawl4AI выполнить JavaScript в уже загруженной странице вместо того, чтобы загружать URL заново. Перезагрузка обходится во вторую загрузку страницы, на которой проверка может выпасть снова, и сбрасывает всё, что подготовила первая загрузка, например частично заполненную форму.
import json
async def submit(crawler, token):
# Crawl4AI wraps this in an async function, so statements work.
inject = (
"document.getElementById('g-recaptcha-response').value = "
f"{json.dumps(token)};"
"document.querySelector('form').submit();"
)
config = CrawlerRunConfig(
session_id=SESSION,
js_only=True, # same tab, no reload
js_code_before_wait=inject, # runs BEFORE wait_for
wait_for="css:.signup-complete",
)
return await crawler.arun(PAGE_URL, config=config)Вот почему скрипт ставится в js_code_before_wait. Начиная с 0.8.5 и во всех выпусках 0.9 обход сначала выполняет js_code_before_wait, затем wait_for и в самом конце js_code, уже на готовой странице. Поэтому если поставить отправку в js_code, wait_for начнёт искать следующую страницу ещё до того, как что-либо отправлено, и будет искать, пока не истечёт page_timeout (по умолчанию 60 секунд), после чего вызов упадёт с Wait condition failed, а форма так и не уйдёт. Примеры, которые отправляют форму из js_code и ждут через wait_for в том же вызове, включая один пример в собственной документации Crawl4AI, на 0.9.4 упираются именно в это.
Собирайте строку через json.dumps, а не вставляйте токен между кавычками, потому что строковый литерал JSON одновременно является корректным литералом JavaScript, вместе с экранированием. Ждать навигацию самостоятельно не нужно: wait_for продолжает опрашивать страницу и сквозь навигацию, а .signup-complete находит уже на следующей странице. Чего Crawl4AI делать не станет, так это бросать исключение, когда скрипт падает. Синтаксическая ошибка даёт строку в логе, а ошибка во время выполнения, например опечатка в ID элемента, проглатывается без всякой записи и проявляется только как таймаут wait_for, поэтому проверьте эти две инструкции в консоли DevTools на настоящей странице, прежде чем винить решатель.
Некоторые сайты вообще не читают textarea. Они регистрируют у виджета callback и отправляют форму оттуда, поэтому замените две инструкции вызовом функции, указанной в атрибуте data-callback, передав ей токен. Под капотом это всё та же обычная reCAPTCHA v2, как объясняется на странице решателя reCAPTCHA v2.
Шаг 4: решение внутри глубокого обхода с помощью хука
Сценарий с сессией справляется с капчей в Crawl4AI, когда каждый вызов arun в ваших руках. Глубокий обход или пакет arun_many такой возможности не дают, поэтому привяжите решение к хуку after_goto. Он выполняется на странице Playwright сразу после навигации и до wait_for, для каждого URL, на который переходит краулер (вызовы с js_only его пропускают), так что страницы без виджета проходят насквозь.
from capskip import CapSkipError
async def after_goto(page, context, url, response, **kwargs):
holder = await page.query_selector("div.g-recaptcha[data-sitekey]")
if holder is None:
return page # no widget, nothing to do
sitekey = await holder.get_attribute("data-sitekey")
try:
result = await solver.recaptcha(sitekey=sitekey, url=page.url)
except CapSkipError:
return page # keep the page HTML if the solve fails
async with page.expect_navigation():
await page.evaluate(
"t => { document.getElementById('g-recaptcha-response').value = t;"
" document.querySelector('form').submit(); }", result["code"])
return page
crawler.crawler_strategy.set_hook("after_goto", after_goto)Об этом пути стоит знать несколько вещей. Хук выполняется внутри обхода, поэтому время решения прибавляется к этой странице, а если на виджет одновременно натыкаются несколько страниц, каждая ждёт своего решения, и AsyncCapSkip обрабатывает их бок о бок. Хук глобален для всего краулера, поэтому проверка должна быть дешёвой: её выполняет каждая страница. И блок try здесь важен: исключение, вылетевшее из хука, валит весь обход этого URL, и HTML не возвращается вовсе, так что без try сбой CapSkip опустошил бы каждую защищённую страницу в глубоком обходе. Ещё одна ловушка: Crawl4AI сохраняет код статуса первого ответа. Проверка, отданная с кодом 403, всё равно возвращается с success, равным False, и с Blocked by anti-bot protection, даже если хук её решил и в result.html лежит страница, которая за ней скрывалась. На таких URL ищите в html свой контент, а не доверяйте success. Каждый хук и его аргументы Crawl4AI описывает на своей странице о хуках. Тот же хук подходит любому краулеру на базе Playwright, а более широкая картина изложена на странице сервиса распознавания капч для Playwright.
Где работает CapSkip, когда краулер на другой машине
Когда обход разрастается, Crawl4AI часто оказывается на Linux-сервере или в контейнере, а CapSkip представляет собой приложение для Windows, так что они нередко живут на разных машинах. Именно для этого и нужен режим Server. Он заставляет CapSkip слушать ваш сетевой адрес или публичный IP вместо 127.0.0.1, и краулер обращается к нему по тому же HTTP API отовсюду, откуда до него есть маршрут. Если этот маршрут идёт через интернет, используйте статический публичный IP, включите проверку API-ключа и ограничьте порт адресами краулера с помощью правила Windows Firewall. Это по-прежнему машина, которой владеете вы, и решение на ней по-прежнему не тарифицируется.
SDK сам не читает переменные окружения. Читайте CAPSKIP_HOST, CAPSKIP_PORT и CAPSKIP_API_KEY в своём коде и передавайте их явно, как это делает полный пример.
Одну деталь Crawl4AI нужно учесть заранее: начиная с 0.9.0 его Docker-сервер отклоняет session_id, js_code и js_code_before_wait с HTTP 400, если они приходят по сети, и больше не принимает код хуков. Ни один из путей в этом руководстве не пройдёт через REST API, поэтому используйте библиотеку в своём собственном процессе Python.
Полный рабочий пример
# pip install crawl4ai capskip
import asyncio
import json
import os
import re
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
from capskip import AsyncCapSkip
PAGE_URL = "https://example.com/signup"
SESSION = "signup"
WIDGET = r'<[^>]*class="(?:[^"]*\s)?g-recaptcha(?:\s[^"]*)?"[^>]*>'
solver = AsyncCapSkip(
apiKey=os.getenv("CAPSKIP_API_KEY", "capskip"),
host=os.getenv("CAPSKIP_HOST", "127.0.0.1"),
port=int(os.getenv("CAPSKIP_PORT", "8080")),
)
async def main():
async with AsyncWebCrawler() as crawler:
first = await crawler.arun(
PAGE_URL, config=CrawlerRunConfig(session_id=SESSION))
widget = re.search(WIDGET, first.html)
match = widget and re.search(r'data-sitekey="([^"]+)"', widget.group(0))
if not match:
raise RuntimeError(f"no sitekey found: {first.error_message}")
result = await solver.recaptcha(sitekey=match.group(1), url=PAGE_URL)
inject = (
"document.getElementById('g-recaptcha-response').value = "
f"{json.dumps(result['code'])};"
"document.querySelector('form').submit();"
)
done = await crawler.arun(PAGE_URL, config=CrawlerRunConfig(
session_id=SESSION,
js_only=True,
js_code_before_wait=inject,
wait_for="css:.signup-complete",
wait_for_timeout=30000,
))
print(done.success) # True once the next page has loaded
asyncio.run(main())wait_for_timeout задаёт ожиданию после отправки собственный лимит, так что отправка, которая никуда не ведёт, падает через 30 секунд, а не через 60 секунд page_timeout. Замените .signup-complete на что угодно, что есть только на странице после формы. Об остальном ландшафте Python, включая Selenium и обычные HTTP-клиенты, читайте на странице сервиса распознавания капч для Python.
Частые ошибки и что они означают
| Что вы видите | Причина | Исправить |
|---|---|---|
| Wait condition failed примерно через 60 секунд, а форма так и не отправилась | Отправка стоит в js_code, который с 0.8.5 выполняется после wait_for | Перенесите скрипт в js_code_before_wait |
| first.success равен False с Blocked by anti-bot protection | Проверка блокировок в Crawl4AI приняла страницу с капчей за страницу блокировки | Всё равно считайте sitekey из first.html; страница цела |
| wait_for уходит в таймаут на втором вызове, а html содержит пустую страницу | session_id отличается, поэтому Crawl4AI открыл новую пустую вкладку | Используйте один и тот же session_id в обоих вызовах |
| Токен в textarea есть, но сайт сообщает, что капча не пройдена | Сайт отправляет форму через функцию из data-callback, или токен истёк | Вызовите callback с токеном и отправьте форму в течение двух минут после решения |
| Никакой ошибки, а потом ожидание уходит в таймаут | Скрипт вставки выбросил исключение внутри страницы, а Crawl4AI молча это проглатывает | Сначала выполните скрипт в консоли DevTools и проверьте ID, которые он использует |
| HTTP 400 от Docker-сервера Crawl4AI | Сервер отклоняет session_id и поля со скриптами, пришедшие по сети | Запускайте библиотеку в своём собственном процессе Python |
| NetworkException при решении | CapSkip не запущен либо хост и порт указывают не на ту машину | Запустите CapSkip и используйте режим Server, если краулер находится на другой машине |
| TimeoutException при решении | Решение заняло больше, чем recaptchaTimeout | Поднимите его в конструкторе выше значения по умолчанию в 300 секунд |
FAQ
Решает ли Crawl4AI капчи сам?
Нет. Он их обнаруживает в том смысле, что его антибот-проверка помечает страницу с капчей как заблокированный обход, и умеет повторять попытку через список прокси, если страница заблокирована. Опции magic и simulate_user добавляют движения мыши и прокрутку, которые ищут антибот-системы, и это может снизить вероятность проверки. Но ничто из этого не решает виджет, если он уже на странице: этот пробел и закрывает внешний решатель, и он же описан на странице сервиса распознавания капчи для парсинга.
Работает ли тот же сценарий для Cloudflare Turnstile?
Для виджета да. Вызовите solver.turnstile с sitekey и URL страницы и запишите токен в скрытый input с именем cf-turnstile-response вместо textarea reCAPTCHA. Полностраничные проверки представляют собой отдельную задачу, потому что их токен должен идти вместе с user agent, который возвращает CapSkip, а значит, браузер, отправляющий его, должен предъявить тот же самый user agent. Этот случай разобран отдельно: загляните на страницу решателя Cloudflare Turnstile.
Мой краулер работает в контейнере. Куда ставить CapSkip?
На машину с Windows, которой вы управляете, с включённым режимом Server. Контейнер после этого обращается к нему через API, как к любому другому внутреннему сервису, поэтому краулеру и решателю не нужна общая операционная система. Передайте адрес машины с Windows в CAPSKIP_HOST, включите проверку API-ключа и выдайте краулеру собственный ключ, чтобы его можно было отозвать отдельно.
Сколько сессия остаётся открытой между двумя вызовами?
Гораздо дольше, чем длится решение. Менеджер браузера в исходниках 0.9.4 убирает сессии, которые не использовались 30 минут, а закрытие краулера закрывает их все. На практике важен лимит токена, около двух минут, поэтому решение и отправка должны следовать друг за другом без паузы.
Коротко
Вот весь процесс решения капчи в Crawl4AI в одном абзаце. Загрузите страницу с session_id и считайте sitekey из HTML, даже если Crawl4AI называет обход заблокированным. Решите капчу через AsyncCapSkip. Вернитесь в ту же вкладку с js_only=True, вставьте токен и отправьте форму из js_code_before_wait, а следующую страницу ждите через wait_for с собственным таймаутом. Для глубоких обходов делайте то же самое из хука after_goto. Если краулер работает на другой машине, направьте клиент на адрес в режиме Server.
Напоследок об одной вещи, которая меняет подход к масштабу парсинга. Поскольку обход капчи выполняется на машине, которой вы уже владеете, парсинг, где виджет встречается на каждой странице, стоит столько же, сколько и тот, где он встретился всего раз, поэтому нет причин пропускать URL только из-за того, что он закрыт проверкой.
