Как решать капчу в httpx и aiohttp (без браузера)

Поток решения капчи на httpx укладывается в три шага и примерно двадцать строк, а причина ошибок обычно не имеет никакого отношения к решателю. В этом стеке нет браузера. JavaScript сайта никто не выполняет, поэтому никто не создаёт скрытое поле, в которое должен попасть токен, и никто не отправляет за вас форму. Все три шага вы делаете сами: считываете sitekey из HTML, решаете капчу, а затем отправляете токен обратно обычным полем формы тем же клиентом, который загрузил страницу.
Что понадобится
- Python 3.10 или новее и httpx либо aiohttp. CapSkip Python SDK работает с обоими.
- URL защищённой страницы. Знать sitekey заранее не нужно: его считывает первый шаг.
- Запущенный CapSkip в режиме Local, когда скрипт и решатель работают на одной машине, или в режиме Server, когда это не так. Оба режима описаны в разделе Настройки подключения.
# pip install capskip pip install capskip httpx # Using aiohttp instead? Install it as well. See the note below # about which HTTP libraries you end up with either way. pip install capskip aiohttp
httpx уже идёт в комплекте с SDK
Это стоит знать до того, как вы начнёте считать зависимости. Пакет CapSkip для Python объявляет requests, httpx и aiofiles обязательными зависимостями времени выполнения, поэтому установка SDK ставит httpx независимо от того, просили вы его или нет. Если httpx и так ваш клиент для парсинга, решатель не добавляет в окружение ни одной новой HTTP-библиотеки, а AsyncCapSkip работает в вашем же event loop. Если вы на aiohttp, httpx приедет рядом с ним, и в одном virtualenv окажется два клиента. Ничего не сломается, но это как раз то, о чём спросят на ревью зависимостей.
Что меняется, когда браузера нет
В Selenium или Playwright вы задаёте значение скрытой textarea, которую отрисовывает reCAPTCHA, и собственный обработчик отправки страницы забирает его дальше. Здесь ничего этого нет. HTTP-клиент забирает байты, а теги script никто не выполняет, поэтому виджет не отрисовывается, textarea не создаётся, и запускать нечего: обработчика отправки просто не существует.
То, что есть вместо этого, проще и понятнее. Токен представляет собой обычную строку, и сайт ждёт её в теле POST-запроса под определённым именем поля. Для reCAPTCHA v2 это поле называется g-recaptcha-response, и имя одно и то же независимо от того, заполнил его браузер или вы. Turnstile использует cf-turnstile-response. GeeTest отправляет не одно поле, а три. Проверка происходит на сервере сайта через Google или Cloudflare, поэтому на другом конце никто не может определить, какой из ваших процессов породил эту строку.
Шаг 1: считайте sitekey тем клиентом, которым будете отправлять форму
Используйте один клиент на весь поток. Дело не в аккуратности. В этом вся суть: именно хранилище cookie и пул соединений делают так, что POST выглядит пришедшим от того же посетителя, что и GET. Соберёте новый клиент под отправку, и вы выбросите все сессионные cookie, которые поставила страница, а это очень частая причина, по которой правильный токен всё равно не проходит.
# pip install capskip httpx
import re
import httpx
PAGE_URL = "https://example.com/page-with-recaptcha"
# One client for the whole flow. Its cookie jar is what makes
# the POST later look like the same visitor as this GET.
client = httpx.Client(timeout=30, follow_redirects=True)
html = client.get(PAGE_URL).text
match = re.search(r'data-sitekey=["\']([^"\']+)', html)
if not match:
raise RuntimeError("No data-sitekey in the HTML.")
sitekey = match.group(1) # this is what the solver needsЕсли это регулярное выражение ничего не находит, значит виджет подставляется через JavaScript, а не отдаётся в разметке, и HTTP-клиент его никогда не увидит. Откройте страницу в браузере, один раз возьмите sitekey из вкладки сети и пропишите его в коде. Sitekey публичен и стабилен, поэтому стыдиться такого сокращения пути не стоит.
Шаг 2: решите капчу на своей машине
Решение капчи сводится к одному вызову сервиса, который слушает на вашем собственном оборудовании. Все варианты reCAPTCHA обслуживает один и тот же метод, отличаются только именованные аргументы: invisible со значением 1, enterprise со значением 1 или version со значением v3 вместе с action. У Turnstile и GeeTest есть свои методы той же формы. Полный список параметров приведён в документации CapSkip API.
# CapSkip listens on your machine, so this is a loopback call. from capskip import CapSkip solver = CapSkip(host="127.0.0.1", port=8080) token = solver.recaptcha(sitekey=sitekey, url=PAGE_URL)["code"]
Этот вызов блокирует выполнение, пока идёт опрос. SDK опрашивает не с фиксированным интервалом: он начинает с 250 миллисекунд и постепенно увеличивает паузу до pollingInterval, поэтому решение через SDK обычно возвращается быстрее, чем цикл, написанный вручную поверх сырого эндпоинта. Верхний предел задаёт recaptchaTimeout, по умолчанию 300 секунд.
Шаг 3: отправьте токен полем формы
Теперь отправьте его обратно тем же клиентом, в той же форме, которую отправил бы браузер. Включите остальные поля формы, в том числе скрытые значения CSRF или nonce, которые вы считали из HTML на первом шаге.
# g-recaptcha-response is an ordinary form field. Same client,
# so the session cookies from the GET go along with it.
reply = client.post(
PAGE_URL,
data={
"username": "demo",
"g-recaptcha-response": token,
},
)
print(reply.status_code)Отправляйте токен сразу. Токен reCAPTCHA действителен около двух минут, и очередь, отступ перед повтором или sleep между решением и отправкой сожгут этот запас незаметно для вас. Срок жизни токена и то, что он означает на практике, разобраны в руководстве по сроку действия токена reCAPTCHA.
Полный рабочий пример, асинхронная версия
Те же три шага с асинхронным клиентом. AsyncCapSkip в Python представляет собой настоящую асинхронную реализацию, а не псевдоним синхронной, поэтому он работает в вашем event loop и не занимает отдельный поток на время опроса.
# pip install capskip httpx
import asyncio
import re
import httpx
from capskip import AsyncCapSkip
PAGE_URL = "https://example.com/page-with-recaptcha"
SITEKEY_RE = re.compile(r'data-sitekey=["\']([^"\']+)')
async def submit_once(client, solver):
html = (await client.get(PAGE_URL)).text
match = SITEKEY_RE.search(html)
if not match:
raise RuntimeError("No data-sitekey in the HTML.")
result = await solver.recaptcha(sitekey=match.group(1), url=PAGE_URL)
reply = await client.post(
PAGE_URL,
data={"g-recaptcha-response": result["code"]},
)
return reply.status_code
async def main():
solver = AsyncCapSkip(host="127.0.0.1", port=8080)
async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
print(await submit_once(client, solver))
asyncio.run(main())Чтобы запустить несколько таких потоков сразу, соберите их через gather. Одного экземпляра решателя хватит на всю пачку, а клиент обычно нужен свой на каждую личность, потому что общее хранилище cookie означает общую сессию. Про пакетную сторону этой задачи написано в руководстве по параллельному решению капчи на Python.
Версия для aiohttp
Всё сказанное выше остаётся в силе. Меняются только три имени: тип сессии, способ чтения тела ответа и атрибут, в котором лежит код статуса.
# pip install capskip aiohttp
import aiohttp
from capskip import AsyncCapSkip
async def submit_once(session, solver):
async with session.get(PAGE_URL) as reply:
html = await reply.text()
match = SITEKEY_RE.search(html)
result = await solver.recaptcha(sitekey=match.group(1), url=PAGE_URL)
async with session.post(
PAGE_URL,
data={"g-recaptcha-response": result["code"]},
) as submitted:
return submitted.status # not status_code
async def main():
solver = AsyncCapSkip(host="127.0.0.1", port=8080)
async with aiohttp.ClientSession() as session:
print(await submit_once(session, solver))ClientSession хранит собственные cookie, поэтому правило одного клиента переносится без изменений. HTTP/2 в aiohttp не поддерживается, но для этой задачи потеря нулевая.
Прокси и ловушка, которая срабатывает только на HTTP-клиентах
Если сайт проверяет, что токен был создан с того же адреса, с которого его отправляют, решение и отправка должны уходить через один и тот же прокси. CapSkip принимает прокси на каждую задачу для reCAPTCHA, Turnstile и GeeTest. Графические капчи прокси не принимают, и он им не нужен.
# Same exit address for the solve and for the submit.
result = await solver.recaptcha(
sitekey=sitekey,
url=PAGE_URL,
proxy={"type": "HTTP", "uri": "user:[email protected]:3128"},
)
# httpx 0.26 and newer spell this proxy=. Before that it was
# proxies=, which 0.28 removed outright.
async with httpx.AsyncClient(proxy="http://user:[email protected]:3128") as client:
await client.post(PAGE_URL, data={"g-recaptcha-response": result["code"]})Вот место, на котором люди спотыкаются. CapSkip принимает SOCKS5 и SOCKS5H в качестве типов прокси, но aiohttp понимает только HTTP-прокси плюс HTTPS через туннель CONNECT, а httpx вообще не станет работать с SOCKS, пока не установлен дополнительный пакет socks. Отдайте решателю прокси SOCKS5, которым ваш клиент пользоваться не умеет, и решение пройдёт с одного адреса, а отправка уйдёт с другого. Выглядит это ровно как испорченный токен, хотя токен исправен. Держите оба конца на том типе прокси, который они оба понимают. Выбор между типами прокси разобран в руководстве по ротации прокси при решении капчи.
Ещё одна особенность aiohttp: в отличие от requests, он игнорирует переменные окружения HTTP_PROXY и HTTPS_PROXY, пока вы не создадите сессию с trust_env со значением True. Прокси, который вы считали применённым, просто не применялся, и никто вам об этом не сказал.
HTTP/2 и что меняется при его включении
httpx умеет работать по HTTP/2, но не по умолчанию и не без дополнительного пакета: установите httpx с extra под названием http2 и передайте http2 со значением True при создании клиента. Стоит знать, что это меняет вид вашего трафика на проводе, потому что согласование протокола и порядок заголовков отличаются от HTTP/1.1. Это вопрос фингерпринтинга, а не решения капчи, и тема отдельная, намеченная в материале о TLS-фингерпринтинге в Python.
Запуск решателя на другой машине
Скрипты переезжают. Контейнер, VPS или воркер по расписанию работают не на той машине, где стоит решатель, и loopback там указывает на сам контейнер, где никто не слушает.
У CapSkip два режима подключения. Local привязывается к 127.0.0.1 и отвечает только этому устройству. Server привязывается к вашему сетевому или публичному IP, поэтому контейнер, виртуальная машина или размещённый воркер обращаются к той же машине с Windows через API. Статический публичный IP сохраняет этот адрес неизменным. В обоих случаях это по-прежнему ваше оборудование и по-прежнему без тарификации, поэтому загруженный день стоит одинаково в любом режиме.
# The SDK reads these three itself, so the same code works # whether the solver is local or on another machine: # CAPSKIP_HOST=192.0.2.10 # CAPSKIP_PORT=8080 # CAPSKIP_API_KEY=your-key solver = AsyncCapSkip()
Включите проверку ключей, как только решатель начнёт слушать сетевой адрес, и выдайте каждому воркеру собственный ключ, чтобы один можно было отозвать, не трогая остальные. Оба режима подробно разобраны в руководстве по настройке CapSkip.
Частые ошибки и что они означают
| Что вы видите | Причина | Исправить |
|---|---|---|
| Регулярное выражение не находит data-sitekey | Виджет подставляется через JavaScript, поэтому его нет в отданном HTML | Считайте sitekey один раз в браузере и пропишите его в коде. Он публичный и стабильный |
| Форма снова возвращается с капчей | Для POST был создан второй клиент, поэтому сессионные cookie потерялись | Используйте один клиент и для GET, и для POST |
| Действительный токен отклоняется | Решение и отправка ушли с разных адресов | Используйте один и тот же прокси на обеих операциях, причём такого типа, который понимают обе стороны |
| Действительный токен отклоняется после задержки | Он истёк в очереди между решением и отправкой | Решайте как можно позже и отправляйте сразу |
| Кажется, что aiohttp игнорирует прокси | aiohttp по умолчанию не читает переменные окружения с прокси | Передайте proxy= явно или создайте сессию с trust_env со значением True |
| TypeError в конструкторе клиента httpx | proxies= удалён в httpx 0.28 | Используйте proxy=: это имя действует начиная с версии 0.26 |
| NetworkException | CapSkip не запущен либо неверны хост и порт | Запустите приложение или направьте CAPSKIP_HOST на адрес сервера |
| ValidationException | Аргумент, который SDK не принимает для этого типа капчи | Проверьте тип. Отправка action на v2 или invisible на v3 вызывает эту ошибку |
FAQ
Действительно ли reCAPTCHA v2 работает без браузера?
Да. Сервер сайта проверяет токен через Google, и эта проверка смотрит на токен, на sitekey и на адрес, с которого он пришёл. Спросить, какой процесс создал строку, она никак не может. Браузер всегда был лишь удобным местом для этого поля.
Навязывает ли установка SDK библиотеку httpx?
Да. requests, httpx и aiofiles объявлены зависимостями пакета, поэтому httpx приезжает вместе с ним даже в проекте на aiohttp. Это второй HTTP-клиент в окружении, а не конфликт, и ваш собственный код не обязан им пользоваться.
Как сделать то же самое для reCAPTCHA v3?
Передайте version со значением v3 и то значение action, которое использует страница, а затем отправьте токен под тем именем поля, в которое его записывает собственный скрипт сайта. Это имя не стандартизовано так, как g-recaptcha-response, поэтому один раз считайте его со страницы. Значение action должно совпадать, иначе проверка на стороне сервера не пройдёт, даже если токен настоящий.
Что выбрать для этой задачи: httpx или aiohttp?
Подойдёт любой. httpx и так ставится вместе с SDK и умеет HTTP/2 и SOCKS при установленных дополнительных пакетах, поэтому этот вариант не добавляет в ваше окружение ничего нового. aiohttp быстрее при очень высокой параллельности, и на нём построено множество существующих краулеров. Код решателя одинаков в обоих случаях, поэтому выбирайте по достоинствам самого краулера, а не по стороне капчи.
Коротко
Считайте sitekey из отданного HTML, решите капчу локально, затем отправьте токен обычным полем формы тем клиентом, который загрузил страницу. Один клиент на весь поток, чтобы cookie уцелели. Один и тот же исходящий адрес для решения и для отправки, на типе прокси, который действительно понимают обе стороны. Отправляйте сразу, потому что токен живёт недолго.
Более широкая картина по Python есть на странице сервиса распознавания капч для Python. Подробности о reCAPTCHA v2 находятся на странице сервиса распознавания reCAPTCHA v2. Те же три вызова есть и в Node.js, PHP и C#, и они перечислены на на странице SDK для распознавания капчи.
Последнее, что стоит знать, прежде чем направлять всё это на большой обход. CapSkip представляет собой безлимитный сервис распознавания капчи работающий на оборудовании, которое у вас уже есть, поэтому обход, решающий пятьдесят тысяч капч, стоит ровно столько же, сколько обход, решающий пятьдесят.
