Как решить CaptchaFox на Python с помощью requests и httpx

Чтобы решить CaptchaFox на Python, вызовите solver.captchafox() из пакета capskip с sitekey и URL страницы, положите полученный токен в поле формы cf-captcha-response и отправьте этот запрос с тем user agent, который вернулся вместе с токеном. Именно на последнем шаге код на Python обычно и ошибается. И requests, и httpx по умолчанию представляются в заголовке User-Agent своими именами, а сайт, который ждёт токен от выпустившего его браузера, такой запрос отклоняет, так что безупречно прошедшее решение всё равно может провалиться при отправке. CapSkip добавил CaptchaFox в версии 1.4.0. В этом руководстве разобраны ключ, вызов, отправка, повторы и параллельный запуск решений.
Что понадобится
- CapSkip 1.4.0 или новее на машине с Windows. Именно в этом выпуске появился CaptchaFox, а вместе с ним Friendly Captcha и Capy Puzzle.
- Python 3.10 или новее и пакет capskip версии 1.3.0 или новее: это первый выпуск, в котором есть метод captchafox(). В примерах также используется requests, а в разделе о параллельных решениях используется httpx.
- Два значения с целевой страницы: sitekey и URL страницы, на которой работает виджет. Шаг 1 показывает, где лежит ключ и на какой тег script заодно стоит посмотреть.
- Адрес решателя. В режиме Local CapSkip отвечает на 127.0.0.1 и только для этого устройства; в режиме Server он слушает ваш сетевой адрес или публичный IP, чтобы скрипт на другой машине мог обращаться к нему через API. Оба режима задаются в разделе Настройки подключения.
# Quoted, so cmd.exe does not read >= as a redirect pip install "capskip>=1.3.0" requests httpx
Шаг 1: читаем sitekey и источник виджета
Sitekey публичен, одинаков для всех посетителей и по соглашению начинается с sk_. На большинстве страниц он указан на контейнере виджета: это div с классом captchafox и атрибутом data-sitekey. Страница, которая строит виджет собственным скриптом, вместо этого передаёт ключ в вызов captchafox.render, а если ни того, ни другого нет в полученном HTML, ключ находится на вкладке Network: это сегмент пути после /captcha/ в запросе к api.captchafox.com.
Раз уж вы читаете страницу, проверьте, какой скрипт загружает виджет. Большинство сайтов загружают его с cdn.captchafox.com и получают обычный токен. Некоторые платформы вместо этого встраивают пакет с s.uicdn.com/mampkg/, и такие сайты ждут токен, который начинается с MAM_. Если решать не для того варианта, токен всё равно вернётся, но в формате, который сайт потом отклонит, так что эта проверка сэкономит вам полдня недоумения. На страницах с MAM ключ может находиться прямо в src самого скрипта, после ?key=, а тот же CDN раздаёт и посторонние пакеты, поэтому сверяйте путь к пакету captchafox, а не хост.
import re import requests PAGE_URL = "https://example.com/signup" session = requests.Session() html = session.get(PAGE_URL, timeout=30).text # The container: <div class="captchafox" data-sitekey="sk_..."> tag = re.search(r'<[^>]*class="(?:[^"]*\s)?captchafox(?:\s[^"]*)?"[^>]*>', html) key = tag and re.search(r'data-sitekey="([^"]+)"', tag.group(0)) # MAM pages may carry the key in the script src instead. key = key or re.search(r'captchafox[^"]*/api\.js\?key=([^"&]+)', html) sitekey = key.group(1) if key else None # The MAM build expects a MAM_ token; everything else is the default. uses_mam = "mampkg/@mamdev/core.frontend.libs.captchafox" in html print(sitekey, uses_mam)
Шаблон для класса ограничен пробельными символами, поэтому элемент-обёртка с классом вроде captchafox-container не совпадёт случайно. Шаблон ожидает двойные кавычки вокруг значений атрибутов: именно так их пишет собственный сниппет CaptchaFox.
Шаг 2: вызов captchafox()
Чтобы решить CaptchaFox на Python, нужен один метод: он принимает sitekey и URL страницы, а также необязательные именованные аргументы. Для стандартного виджета ему хватает этих двух значений.
# pip install "capskip>=1.3.0"
from capskip import CapSkip
solver = CapSkip(host="127.0.0.1", port=8080)
result = solver.captchafox("YOUR_SITEKEY", "https://example.com/signup")
print(result["token"]) # goes in cf-captcha-response
print(result.get("userAgent")) # send this as the User-AgentРезультат представляет собой обычный dict. token и code содержат одну и ту же строку, а token назван по имени поля, которое он заполняет. userAgent описывает браузер, который выпустил токен, то есть собственный браузер CapSkip, а не что-либо отправленное вами. Этот ключ есть, только если решение его сообщило, поэтому читайте его через result.get(), а не через квадратные скобки, иначе редкое решение без него вызовет KeyError на той строке, которая должна была отправить форму.
URL страницы должен указывать на ту страницу, где виджет действительно работает. CaptchaFox регистрирует каждый ключ на список разрешённых доменов и проверяет хост, прежде чем что-либо выдать, поэтому ключ, переданный с неправильной страницей, отклоняется каждый раз, а не время от времени. Редирект, сокращённая ссылка или результат поиска не подойдут.
Опции, которые принимает метод
Для страницы, которая загружает пакет MAM, передайте путь к пакету из её тега script в api_server. SDK отбрасывает любой именованный аргумент со значением None ещё до отправки запроса, поэтому опцию можно передавать при каждом вызове, а выбор оставить проверке из шага 1.
# Copied from the script tag on MAM pages.
MAM_PACKAGE = "https://s.uicdn.com/mampkg/@mamdev/core.frontend.libs.captchafox/"
result = solver.captchafox(
sitekey,
PAGE_URL,
# None is dropped, so the standard widget sends nothing extra.
api_server=MAM_PACKAGE if uses_mam else None,
)Помимо api_server, captchafox() принимает proxy, proxytype и useragent, а также timeout на один вызов и polling_interval в секундах. Опция useragent принимается для совместимости с другими сервисами и намеренно не применяется, потому что CapSkip решает в настоящем браузере, у которого собственный, внутренне согласованный набор признаков. Любой другой именованный аргумент, пустой sitekey или URL, словарь proxy без type и uri, а также тип прокси, отличный от HTTP, HTTPS, SOCKS5 или SOCKS5H, вызывают ValidationException ещё до отправки запроса.
Шаг 3: отправляем под тем user agent, с которым выпущен токен
Когда вы решаете CaptchaFox на Python, именно этот шаг определяет, примет ли сайт токен. Из коробки requests отправляет User-Agent вида python-requests с номером своей версии, а httpx отправляет python-httpx со своим. Ни то, ни другое не является браузером, который создал токен. Скопируйте возвращённое значение в запрос, который несёт токен.
# session is the one Step 1 fetched the page with, so it carries its cookies.
ua = result.get("userAgent")
resp = session.post(
PAGE_URL, # or wherever the form's action attribute points
data={"email": "YOUR_EMAIL", "cf-captcha-response": result["token"]},
# Per request, not on session.headers: the next token may differ.
headers={"User-Agent": ua} if ua else {},
timeout=30,
)
print(resp.status_code)Заголовок, переданный в post(), накладывается поверх значений сессии по умолчанию только для этого запроса, поэтому сессия сохраняет свои cookies, а все последующие запросы уходят как прежде. Если же задать session.headers, user agent одного решения отпечатается на каждом следующем запросе, включая отправку следующего токена. С httpx загружайте страницу и отправляйте форму через один httpx.Client, чтобы cookies переносились, и передавайте data, headers и timeout в его post() тем же способом.
Ещё два правила следуют из того, как CaptchaFox проверяет токен на стороне сайта. Каждый токен можно проверить только один раз и только в течение короткого времени, поэтому решайте капчу, когда уже готовы отправлять, и отправляйте сразу. И считайте токен непрозрачным: не обрезайте, не перекодируйте и не переформатируйте его. Некоторые сайты отправляют его в теле JSON, а не обычной отправкой формы, поэтому один раз отправьте форму вручную с открытыми DevTools и повторите ровно то, что отправляет страница.
Шаг 4: повторы, типы заданий и таймауты
Какое задание выдаст CaptchaFox, выбираете не вы. Чаще всего при решении не выпадает вообще ничего, потому что браузерных признаков хватает самих по себе, а если появляется слайдер-пазл, CapSkip обычно проходит и его. Задания с выбором изображений и аудиозадания редки и не решаются: они возвращаются в виде ApiException, в сообщении которого есть ERROR_CAPTCHA_UNSOLVABLE, причём за считаные секунды, а не по истечении таймаута. При новой попытке обычно выпадает что-то другое, поэтому повторять можно двумя способами.
- В CapSkip. В разделе CaptchaFox в настройках есть параметр Retries (0-3), по умолчанию 0. Если его поднять, CapSkip сам запускает новую попытку при неподдерживаемом задании, при нехватке времени и при ошибке браузера, но никогда не повторяет ключ, который сайт отклоняет. Каждый дополнительный повтор может добавить к одному вызову до одного Row Timeout, по умолчанию 150 секунд, поэтому, подняв Retries, передавайте в captchafox() более длинный timeout.
- В вашем коде. Перехватите исключение и вызовите метод снова, как показано ниже. Подвох один: через API отклонённый ключ тоже выглядит как нерешаемая капча, поэтому ключ, использованный не на том домене, тоже уходит в повтор. Выдаёт такой ключ то, что он падает за считаные секунды при каждой попытке и ни разу не проходит.
from capskip.exceptions import ApiException
def solve_captchafox(sitekey, url, attempts=3, **options):
for attempt in range(1, attempts + 1):
try:
return solver.captchafox(sitekey, url, **options)
except ApiException as exc:
# Select, audio and unreadable slide draws come back
# unsolvable; redraw. Anything else, such as a wrong
# API key, is final.
if "UNSOLVABLE" not in str(exc) or attempt == attempts:
raisecaptchafox() опрашивает результат с таймаутом клиента recaptchaTimeout, по умолчанию 300 секунд, потому что это настоящая браузерная сессия. У CapSkip есть и собственный таймер. Задание может ждать одного из 10 потоков CaptchaFox (Max. Threads) до 250 секунд (Wait Timeout), а на одну попытку отводится 150 секунд (Row Timeout). Если Retries равен 0 и поток свободен, неудачная попытка доходит до вас в виде ApiException от CapSkip задолго до того, как истекут 300 секунд SDK; если задание долго стоит в очереди, SDK может первым исчерпать свой лимит и вместо этого вызвать TimeoutException.
Решение нескольких токенов одновременно
AsyncCapSkip в Python представляет собой настоящий клиент на asyncio, а не псевдоним, поэтому asyncio.gather запускает несколько решений бок о бок. Используйте его в паре с асинхронным клиентом httpx и держите каждый токен вместе с его собственным user agent, пока токен не будет отправлен.
import asyncio
import httpx
from capskip import AsyncCapSkip
solver = AsyncCapSkip(host="127.0.0.1", port=8080)
async def solve_and_submit(client, sitekey, url, form):
await client.get(url) # the form's cookies land in client
result = await solver.captchafox(sitekey, url)
ua = result.get("userAgent")
return await client.post(
url,
data={**form, "cf-captcha-response": result["token"]},
headers={"User-Agent": ua} if ua else {},
)
async def main(jobs):
async with httpx.AsyncClient(timeout=30) as client:
return await asyncio.gather(
*(solve_and_submit(client, *job) for job in jobs),
return_exceptions=True,
)Действуют два ограничения. По умолчанию CapSkip выполняет 10 решений CaptchaFox одновременно, а остальные ждут свободного потока до 250 секунд, заданных в Wait Timeout, так что gather по пятидесяти задачам при обычном времени решения проходит нормально; для более крупных наборов поднимите Max. Threads или запускайте gather небольшими группами. Кроме того, CaptchaFox оценивает не только браузер, но и сеть, поэтому множество решений с одного адреса повышает частоту заданий. Настройте прокси в разделе CaptchaFox в CapSkip или передавайте при каждом вызове словарь proxy с type и uri, а форму отправляйте через тот же выходной адрес. Подробнее о приёмах работы с asyncio рассказано в руководстве по параллельному решению капчи на Python.
Запуск решателя на другой машине
Адрес 127.0.0.1 подходит, пока скрипт и CapSkip работают на одном ПК с Windows. Когда код на Python переезжает на VPS, в контейнер, на раннер CI или в облачный ноутбук, локальная петля указывает не на ту машину, и первое же решение вызывает NetworkException. Переключите CapSkip в режим Server, и он начнёт слушать ваш сетевой адрес или публичный IP, так что любой из этих вариантов сможет обращаться к нему через тот же API. Если маршрут идёт через интернет, используйте статический публичный IP, включите проверку API-ключа и ограничьте порт ожидаемыми адресами с помощью правила Windows Firewall. Решателем по-прежнему остаётся ваша собственная машина с Windows, и решения по-прежнему не тарифицируются.
SDK сам не читает переменные окружения. Читайте CAPSKIP_HOST, CAPSKIP_PORT и CAPSKIP_API_KEY в своём коде и передавайте их в конструктор, как это делает полный пример, чтобы один и тот же скрипт работал и у вас на столе, и на сервере.
Полный рабочий пример
# pip install "capskip>=1.3.0" requests
import os
import re
import requests
from capskip import CapSkip
from capskip.exceptions import (ApiException, CapSkipError,
TimeoutException, ValidationException)
PAGE_URL = "https://example.com/signup"
# Copied from the script tag on MAM pages.
MAM_PACKAGE = "https://s.uicdn.com/mampkg/@mamdev/core.frontend.libs.captchafox/"
solver = CapSkip(
apiKey=os.getenv("CAPSKIP_API_KEY", "capskip"),
host=os.getenv("CAPSKIP_HOST", "127.0.0.1"),
port=int(os.getenv("CAPSKIP_PORT", "8080")),
)
session = requests.Session()
html = session.get(PAGE_URL, timeout=30).text
tag = re.search(r'<[^>]*class="(?:[^"]*\s)?captchafox(?:\s[^"]*)?"[^>]*>', html)
key = tag and re.search(r'data-sitekey="([^"]+)"', tag.group(0))
key = key or re.search(r'captchafox[^"]*/api\.js\?key=([^"&]+)', html)
if not key:
raise SystemExit("No sitekey in the HTML; find it in DevTools.")
api_server = MAM_PACKAGE if "mampkg/@mamdev/core.frontend.libs.captchafox" in html else None
try:
for attempt in range(1, 4):
try:
result = solver.captchafox(key.group(1), PAGE_URL,
api_server=api_server)
break
except ApiException as exc:
# Select, audio and unreadable slide draws come back unsolvable.
if "UNSOLVABLE" not in str(exc) or attempt == 3:
raise
print(f"attempt {attempt}: {exc}")
except ValidationException as exc:
raise SystemExit(f"not sent: {exc}")
except TimeoutException:
raise SystemExit("gave up waiting; recaptchaTimeout is 300 seconds")
except CapSkipError as exc:
# A third unsolvable draw, a refused key, or CapSkip unreachable.
raise SystemExit(f"solve failed: {exc!r}")
ua = result.get("userAgent")
resp = session.post(
PAGE_URL, # or wherever the form's action attribute points
data={"email": "YOUR_EMAIL", "cf-captcha-response": result["token"]},
headers={"User-Agent": ua} if ua else {},
timeout=30,
)
print(resp.status_code, "UA sent:", bool(ua))Если решение мгновенно проваливается при каждом запуске, первым делом проверьте URL страницы, потому что ключ, использованный вне зарегистрированных для него доменов, каждый раз отказывает одинаково. Если регулярное выражение ничего не находит, значит, страница строит виджет скриптом, и ключ находится в вызове render или на вкладке Network, как описано в шаге 1. Сырой эндпоинт, который стоит за этим методом, описан в справочнике API CaptchaFox.
Частые ошибки и что они означают
| Что вы видите | Причина | Исправить |
|---|---|---|
| Сайт отклоняет токен, который CapSkip вернул как решённый | Отправка ушла от имени python-requests или python-httpx, а не от имени браузера, который выпустил токен | Отправьте result.get("userAgent") в заголовке User-Agent этого запроса |
| Отклоняется, хотя user agent совпадает | Страница загружает пакет MAM, а решение использовало виджет по умолчанию, или наоборот | Прочитайте тег script и задайте api_server в соответствии с ним |
| KeyError на userAgent | Решение не сообщило user agent, а ключ есть, только когда оно его сообщает | Читайте его через result.get() и отправляйте заголовок, только если значение есть |
| ApiException сразу же, каждый раз, для одного ключа | URL страницы находится вне доменов, для которых зарегистрирован ключ, или неверен сам ключ | Передавайте страницу, на которой работает виджет, а не редирект или результат поиска, и перепроверьте ключ |
| Время от времени ApiException с ERROR_CAPTCHA_UNSOLVABLE | CaptchaFox выдал задание с выбором изображений или аудиозадание либо вариант слайдера, который CapSkip не смог прочитать | Повторите попытку в коде или поднимите Retries в настройках CaptchaFox в CapSkip |
| Токен, который сработал один раз, не проходит во второй | Каждый токен проходит проверку один раз и быстро истекает | Получайте свежий токен для каждой отправки и отправляйте сразу |
| По ходу прогона заданий всё больше, а затем идут отказы | Все решения идут с одного адреса, а CaptchaFox оценивает сеть | Распределите решения по пулу прокси и отправляйте форму через тот же выходной адрес |
| ValidationException ещё до какой-либо отправки | Пустой sitekey или URL, именованный аргумент, который метод не принимает, или тип прокси, который CapSkip не поддерживает | Проверьте, что sitekey найден, и исправьте или уберите аргумент, названный в сообщении |
| NetworkException на первом вызове | CapSkip не запущен либо неверны хост и порт | Запустите CapSkip и проверьте, в каком режиме он должен работать: Local или Server |
FAQ
Отличается ли result["code"] от result["token"]?
Нет, для CaptchaFox это одна и та же строка. code представляет собой поле, которое заполняет каждый метод CapSkip, благодаря чему старый код продолжает работать без изменений, а token назван так, как этого ждёт форма. В новом коде используйте token: так имя в коде совпадает с полем, которое это значение заполняет.
Можно ли использовать токен в сессии Playwright или Selenium?
Да, если отправка уходит под тем user agent, который вернул CapSkip. Playwright фиксирует user agent при создании контекста, поэтому сначала решите капчу, затем откройте контекст через browser.new_context(user_agent=ua), загрузите форму, запишите токен в поле cf-captcha-response и отправьте. В Selenium с Chrome добавьте в опции Chrome переключатель user-agent с этим значением до запуска драйвера или переключите уже работающую сессию через driver.execute_cdp_cmd("Network.setUserAgentOverride", {"userAgent": ua}). Если браузер больше ни для чего в вашей автоматизации не нужен, описанный выше путь через requests проще.
Может ли скрипт на Python на хостинг-платформе достучаться до решателя?
Да. Переведите CapSkip в режим Server в настройках подключения, чтобы он слушал сетевой адрес, а не локальную петлю, прочитайте этот адрес из CAPSKIP_HOST в своём скрипте и передайте его в CapSkip(). VPS, хост контейнеров, раннер CI и облачный ноутбук подключаются через один и тот же HTTP API. Если маршрут идёт через интернет, используйте статический публичный IP и правило файрвола. Решатель остаётся на вашем собственном железе, поэтому в подсчёте решений ничего не меняется.
Чем это отличается от версии на C#?
Вызов и правила те же, а ловушки разные. В C# HttpClient вообще не отправляет User-Agent, если вы его не добавите. В Python оба популярных клиента вместо этого отправляют собственное имя, python-requests или python-httpx, и сайт может отклонить его с той же лёгкостью. Сторону .NET разбирает руководство по CaptchaFox на C#.
Коротко
Чтобы решить CaptchaFox на Python, считайте sitekey с контейнера captchafox, из вызова render или с вкладки Network и отметьте, загружает ли страница пакет MAM. Вызовите solver.captchafox() с sitekey и настоящим URL страницы, добавляя api_server только для страниц с MAM. Отправьте result["token"] в cf-captcha-response с result.get("userAgent") в заголовке User-Agent, один раз и сразу же. Повторяйте попытку, если выпало нерешаемое задание, добавляйте прокси по мере роста объёма и переходите в режим Server, как только скрипт покидает машину решателя.
- Как работает CaptchaFox и что покрывает решатель: страница решения CaptchaFox.
- Все типы капчи, которые поддерживает пакет для Python, перечислены на странице сервиса распознавания капч для Python.
Единственная привычка, которая нужна этому типу, состоит в том, чтобы повторять попытку при задании с выбором изображений, и когда локальный сервис распознавания капчи работает на вашей собственной машине, каждый повтор стоит нескольких секунд, а не ещё одного оплачиваемого решения.
