Как определить тип капчи на странице и решить её (Python)

Чтобы определить тип капчи на странице, прочитайте в HTML страницы скрипт и контейнер виджета, достаньте из них sitekey и сопоставьте семейство с методом CapSkip, который его решает. Для чекбокса reCAPTCHA v2, скрипта v3 и виджета Turnstile нужны три разных вызова с разными параметрами, а неверная догадка тратит решение впустую и оставляет вам токен, который нельзя использовать. В этом руководстве на Python собирается детектор примерно в 175 строк, который возвращает тип, метод и аргументы или прямо говорит, что определить не может. CapSkip работает на вашем собственном ПК с Windows без платы за каждое решение, поэтому скрипт, проверяющий сто страниц, ничего не добавляет к расходам.
Что понадобится
- CapSkip, запущенный на ПК с Windows, с включёнными нужными вам решателями. Пошаговое руководство по настройке проведёт вас через первый запуск.
- Python 3.10 или новее. Код ниже запускался на Python 3.14.0 и 3.11.7 с requests 2.33.0 и 2.34.2 и capskip 1.3.0.
- Окно PowerShell. Команды написаны в синтаксисе PowerShell.
- Страница, которой вы владеете или которую вам разрешено автоматизировать, с капчей на ней.
- Адрес решателя. В режиме Local CapSkip отвечает на 127.0.0.1 и только для этого устройства. В режиме Server он слушает ваш сетевой адрес или публичный IP, чтобы скрипт на другом ПК, VPS или размещённой платформе мог обращаться к нему через API. Оба режима задаются в разделе Настройки подключения.
Как это проверялось: детектор запускался на 66 сохранённых страницах, и каждая вернула ожидаемый результат. Из них 59 собраны вручную (около 40 следуют документации поставщиков капчи, остальные придуманные мной крайние случаи), а 7 являются снимками публичных страниц от 29 октября 2026 года. Решения выполнялись через настоящий пакет capskip на имитации CapSkip, которая копирует проверки входных данных приложения. Настоящее приложение не запускалось, поэтому ни одно число ниже не является временем решения.
Зачем определять тип капчи до решения
В CapSkip для каждого семейства свой метод: recaptcha, turnstile, geetest, altcha, capy, captchafox, friendly_captcha и normal для изображений в SDK, плюс hcaptcha в HTTP API, для которого в SDK пока нет обёртки. Внутри reCAPTCHA важны и параметры. Invisible, Enterprise и v3 являются флагами того же вызова. Если передать не те, в лучшем случае задание завершится ошибкой, в худшем вы получите токен, который сайт отклонит без объяснений. Я не смог проверить, что именно делает настоящее приложение при неверной версии reCAPTCHA. В любом случае решение потрачено.
Остальные ошибки проявляются быстро и громко. Если взять sitekey со страницы, где reCAPTCHA рендерится явно, получится слово explicit, и CapSkip отклонит его с ошибкой ERROR_GOOGLEKEY. Эту ошибку разбирает руководство по ERROR_GOOGLEKEY и ERROR_PAGEURL с другой стороны. Чтобы надёжно определять тип капчи, детектор должен проверять признаки в фиксированном порядке, и такой порядок исключает оба вида ошибок. Детектор к тому же стоит написать первым, прежде чем делать парсер, который встречает больше одного сайта. Если вам нужна общая картина того, что представляет собой каждый тип, статья о том, что такое капча разбирает все семейства без кода.
Шаг 1: установка и загрузка страницы
Создайте виртуальное окружение в пустой папке и установите два пакета. Команды вызывают Python самого окружения по его пути, поэтому активировать окружение не нужно.
py -m venv .venv .\.venv\Scripts\python.exe -m pip install capskip requests
Чтобы определить тип капчи по тексту, детектору нужны только два входных значения: HTML страницы и её адрес. Загрузите страницу через requests Session и сохраните эту Session, потому что на шаге с капчей-изображением картинка скачивается с теми же cookies. Обычная загрузка через requests показывает то, что сервер отправляет до запуска любого JavaScript. Демо-страницы reCAPTCHA от Google, демо Turnstile от Cloudflare и демо hCaptcha несут свой виджет прямо в этом HTML. Страницы, которые добавляют виджет через JavaScript, так не делают, и этот случай разбирает раздел “Когда HTML ничего не говорит”.
Шаг 2: пишем вспомогательные функции
Сохраните всё отсюда до шага 9 в один файл detect.py, блок за блоком. Первый блок импортирует нужное и пишет четыре небольшие функции чтения. Каждая возвращает None, если ничего не найдено, поэтому отсутствующий атрибут никогда не вызывает исключение.
import base64, html as htmllib, re, time
from types import SimpleNamespace as NS
from urllib.parse import urljoin
import requests
SITEKEY_JS = r"sitekey['\"]?\s*:\s*['\"]([^'\"]+)" # sitekey: '...' inside a render() call
def grab(pattern, text): # first capture group that matched, or None
m = re.search(pattern, text or "", re.I | re.S)
return next(g for g in m.groups() if g is not None) if m else None
def tag(cls, h): # opening tag with CSS class cls (and not g-recaptcha-response)
pat = r'(<\w+[^>]*\sclass\s*=\s*["\'][^"\']*(?<![\w-])%s(?![\w-])[^"\']*["\'][^>]*>)'
return grab(pat % cls, h)
def attr(t, name): # attribute value from a tag string, either quote style, HTML-unescaped
v = grab(r'\s%s\s*=\s*(?:"([^"]*)"|\'([^\']*)\')' % name, t)
return None if v is None else htmllib.unescape(v)
def render_opts(call, js): # first 400 characters of the options in call(el, {...})
return grab(call + r"\([^,]+,\s*\{(.{0,400})", js)Функция grab возвращает первую совпавшую группу захвата. Функция tag находит открывающий тег по CSS-классу и отвергает класс, который лишь входит в состав более длинного, поэтому класс cf-turnstile-response не считается виджетом Turnstile. Функция attr читает атрибут, записанный в любом стиле кавычек, и раскодирует HTML-сущности, что важно для проверки, хранящейся в атрибуте. Атрибуты без кавычек не читаются.
Второй блок превращает страницу в то, чем могут пользоваться все проверки. Он удаляет комментарии и блоки стилей, чтобы CSS-правило с упоминанием класса виджета не могло дать совпадение, а адреса скриптов и встроенный JavaScript собирает отдельно. Последние две вспомогательные функции формируют два возможных ответа: найденный результат (hit) или ответ needs_browser.
def parse(html, url):
h = re.sub(r"<!--.*?-->|<style\b.*?</style>", "", html, flags=re.S | re.I)
srcs = " ".join(re.findall(r'<script\b[^>]*?\ssrc\s*=\s*["\']([^"\']+)', h, re.I))
js = "\n".join(re.findall(r"<script\b[^>]*>(.*?)</script>", h, re.S | re.I))
return NS(h=h, srcs=htmllib.unescape(srcs), js=js, url=url)
def hit(kind, method, **args):
return {"type": kind, "method": method, "args": args}
def browser(kind, method, p, why): # the static HTML is not enough, so do not guess
return {"type": kind, "method": method, "args": {"url": p.url}, "needs_browser": why}Шаг 3: hCaptcha и то, что CapSkip решить не может
Начнём со страниц, на которых правильный ответ «нет». CapSkip не решает FunCaptcha и Arkose, а GeeTest v4 не входит в метод GeeTest, который рассчитан на v3. Если бы детектор их пропускал, страница с Arkose попадала бы в следующую проверку и могла бы вернуться как нечто другое. Поэтому эта проверка идёт первой и возвращает результат без метода.
Тот же блок обрабатывает полностраничную проверку Cloudflare, промежуточную страницу “Just a moment”. Это не виджет Turnstile. Методу turnstile нужны от неё два значения, data и pagedata, а обычный fetch их не получает. Детектор ищет встроенные в страницу параметры проверки и отвечает needs_browser. Что делать дальше, описывает руководство по cf_clearance после того, как страница проверки пройдена.
Следующей идёт hCaptcha, раньше reCAPTCHA. CapSkip решает её, будь виджет чекбоксом, невидимым или Enterprise, но сайт, перешедший с reCAPTCHA на hCaptcha, часто оставляет на виджете класс g-recaptcha, и отличить одно от другого можно только по скрипту hCaptcha. Проверка читает sitekey из виджета или из вызова hcaptcha.render и добавляет invisible, если у виджета data-size равен invisible. Страница Enterprise, которая передаёт виджету значение rqdata через hcaptcha.setData, вместо этого получает needs_browser, потому что это значение выдаётся для одной проверки и быстро истекает.
UNSUPPORTED = {"arkose": r"arkoselabs\.com/v2/|funcaptcha\.com/fc/", "geetest_v4": r"gt4\.js|initGeetest4"}
def unsupported(p): # CapSkip cannot solve these: say so instead of misleading the reader
for kind, pat in UNSUPPORTED.items():
if re.search(pat, p.srcs + " " + p.js):
return {"type": kind, "method": None, "args": {}, "unsupported": True}
def cf_challenge(p): # the full-page "Just a moment..." interstitial, not a widget
if re.search(r"_cf_chl_opt\s*=\s*\{", p.js):
why = "data and pagedata (Cloudflare calls them cData and chlPageData) only exist at run time"
return browser("turnstile_challenge", "turnstile", p, why)
def hcaptcha(p): # before reCAPTCHA: a site that moved to hCaptcha may keep the g-recaptcha class
if not re.search(r"hcaptcha\.com/1/api\.js", p.srcs + " " + p.js):
return None
if re.search(r"\.setData\s*\(|rqdata", p.js): # Enterprise rqdata is issued per challenge
return browser("hcaptcha_enterprise", "hcaptcha", p, "rqdata is issued for one challenge at run time")
w = tag("h-captcha", p.h) or tag("g-recaptcha", p.h)
key = attr(w, "data-sitekey") or grab(SITEKEY_JS, render_opts(r"hcaptcha\.render", p.js))
if not key:
return browser("hcaptcha", "hcaptcha", p, "no sitekey in the static HTML")
args = {"sitekey": key, "url": p.url}
if attr(w, "data-size") == "invisible":
args["invisible"] = 1
return hit("hcaptcha", "hcaptcha", **args)Шаг 4: reCAPTCHA, в правильном порядке
reCAPTCHA требует больше всего внимания, потому что несколько вариантов CapSkip используют два имени скрипта. Разделяет их порядок вопросов. Сначала имя скрипта: enterprise.js означает флаг Enterprise, api.js нет. Затем параметр render в адресе скрипта. Ключ после render= означает v3, а слово explicit после render= означает страницу v2, которая создаёт виджет из кода, и использовать его как ключ нельзя ни в коем случае. Затем разметка. Кнопка с классом g-recaptcha и атрибутом data-action соответствует привязке кнопки v3 от Google. Кнопка без data-action или любой элемент с data-size, равным invisible, является невидимым виджетом v2. Всё остальное с этим классом является чекбоксом.
def recaptcha(p): # Enterprise flag first, then v3 (render=KEY), then v2
m = re.search(r"/recaptcha/(api|enterprise)\.js(\?\S*)?", p.srcs)
if not m:
return None
ent = {"enterprise": 1} if m.group(1) == "enterprise" else {}
render = grab(r"[?&]render=([^&\s]+)", m.group(2)) # a v3 sitekey, or "explicit" (a v2 page)
t, o = tag("g-recaptcha", p.h), render_opts(r"\.render", p.js)
button, action = (t or "").lower().startswith("<button"), attr(t, "data-action")
v3 = bool(render and render != "explicit") or bool(button and action)
key = render if render != "explicit" else None
key = key or attr(t, "data-sitekey") or grab(SITEKEY_JS, o)
if not key:
return browser("recaptcha", "recaptcha", p, "no sitekey in the static HTML (set by JavaScript)")
small = attr(t, "data-size") == "invisible" or grab(r"size['\"]?\s*:\s*['\"](invisible)", o)
invisible = not v3 and bool(button or small)
action = action or grab(r"execute\(\s*[^,)]+,\s*\{\s*['\"]?action['\"]?\s*:\s*['\"]([^'\"]+)", p.js)
args = {"sitekey": key, "url": p.url, **ent}
if v3:
args.update(version="v3", **({"action": action} if action else {}))
if invisible:
args["invisible"] = 1
kind = "recaptcha_" + ("v3" if v3 else "v2_invisible" if invisible else "v2")
return hit(kind + ("_enterprise" if ent else ""), "recaptcha", **args)Каждый исход соответствует знакомому вызову. Чекбокс вызывается как solver.recaptcha с sitekey и URL страницы. Для invisible добавляется invisible=1, для Enterprise добавляется enterprise=1, а для v3 добавляются version="v3" и action. Имя action берётся из атрибута data-action или из вызова execute на странице; если в HTML нет ни того, ни другого, аргумент опускается, и CapSkip использует значение по умолчанию verify. Подробности смотрите на странице решателя reCAPTCHA с описанием каждого поддерживаемого варианта.
Шаг 5: Turnstile
Виджету Turnstile на странице нужны две вещи: api.js от Cloudflare и sitekey. Ключ лежит в атрибуте data-sitekey у div с классом cf-turnstile или в вызове turnstile.render. Проверка требует наличия скрипта, поэтому случайное имя класса на постороннем сайте не вызовет срабатывание. Она также читает data-action и data-cdata, если они есть, и передаёт их дальше. Подробности смотрите на странице решателя Turnstile с полным списком параметров.
def turnstile(p): # widget only: implicit (div.cf-turnstile) or explicit (turnstile.render)
if "challenges.cloudflare.com/turnstile/v0/api.js" not in p.srcs:
return None
t, o = tag("cf-turnstile", p.h), render_opts(r"turnstile\.render", p.js)
key = attr(t, "data-sitekey") or grab(SITEKEY_JS, o)
if not key:
return browser("turnstile", "turnstile", p, "no sitekey in the static HTML")
action = attr(t, "data-action") or grab(r"\baction['\"]?\s*:\s*['\"]([^'\"]+)", o)
cdata = attr(t, "data-cdata") or grab(r"\bcData['\"]?\s*:\s*['\"]([^'\"]+)", o)
extra = {k: v for k, v in (("action", action), ("data", cdata)) if v}
return hit("turnstile", "turnstile", sitekey=key, url=p.url, **extra)Шаг 6: GeeTest и ALTCHA
GeeTest v3 даёт два значения, и одно из них ловушка. Значение gt постоянно для сайта и может лежать на странице. Значение challenge одноразовое, и сайты получают его с эндпоинта register во время выполнения. Если на странице есть оба значения, детектор возвращает их. Если там только вызов initGeetest, он отвечает needs_browser, потому что challenge, найденный в старом HTML, уже был бы недействителен.
ALTCHA менял имена атрибутов от поколения к поколению. Версии 1 и 2 используют challengeurl или challengejson для встроенной проверки. Версия 3 и новее используют один атрибут challenge, который принимает либо URL, либо данные проверки. Проверка читает все три и присоединяет относительный URL к адресу страницы.
def geetest(p): # v3 only: gt is static, challenge comes from the register endpoint
if not (re.search(r"initGeetest\s*\(", p.js) or re.search(r"/gt(\.[\d.]+)?\.js", p.srcs)):
return None
gt = grab(r"\bgt['\"]?\s*:\s*['\"]([0-9a-f]{32})", p.js)
challenge = grab(r"\bchallenge['\"]?\s*:\s*['\"]([0-9a-f]{32,34})", p.js)
if not (gt and challenge):
return browser("geetest", "geetest", p, "gt and challenge come from the register endpoint")
return hit("geetest", "geetest", gt=gt, challenge=challenge, url=p.url)
def altcha(p): # challengeurl / challengejson (v1, v2) or challenge (v3+)
t = grab(r"(<altcha-widget\b[^>]*>)", p.h)
if not t:
return None
c = attr(t, "challenge")
as_json = attr(t, "challengejson") or (c if (c or "").lstrip().startswith("{") else None)
as_url = attr(t, "challengeurl") or (None if as_json else c)
if as_json:
return hit("altcha", "altcha", url=p.url, challenge_json=as_json)
if as_url:
return hit("altcha", "altcha", url=p.url, challenge_url=urljoin(p.url, as_url))
return browser("altcha", "altcha", p, "no challenge attribute")Шаг 7: Capy, CaptchaFox и Friendly Captcha
Эти три легко найти и легко слегка ошибиться. Capy не нужен div виджета для этой проверки. Его ключ начинается с PUZZLE_ и лежит в параметре k адреса скрипта виджета, а хост, отличный от стандартного, передаётся как api_server. CaptchaFox выпускается в двух сборках. Обычная загружается с cdn.captchafox.com. Другую упаковывает хостинг-платформа под общим путём CDN, поэтому детектор ищет в этом пути имя пакета CaptchaFox, а не сам путь, который используют и другие виджеты.
Friendly Captcha самая коварная. Версии 1 и 2 используют один и тот же div и одно пространство sitekey, но разные протоколы, и токен для неверной версии CapSkip принимает, а сайт отклоняет. Скрипты их различают: версия 2 загружает пакет @friendlycaptcha/sdk, версия 1 загружает friendly-challenge. Если страница не даёт признаков, детектор отказывается гадать и отвечает needs_browser.
def capy(p): # the widget script is .../puzzle/get_js/?k=PUZZLE_...
m = re.search(r"((?:https?:)?//[^\s\"']+?)/puzzle/get_js/\S*?(PUZZLE_[\w-]+)", p.srcs)
if m:
host = m.group(1) if m.group(1).startswith("http") else "https:" + m.group(1)
more = {} if host == "https://jp.api.capy.me" else {"api_server": host}
return hit("capy", "capy", sitekey=m.group(2), url=p.url, **more)
MAM = "mampkg/@mamdev/core.frontend.libs.captchafox"
def captchafox(p): # plain build from cdn.captchafox.com, or the bundled build from s.uicdn.com
t, key = tag("captchafox", p.h), grab(r"captchafox[^\"']*/api\.js\?key=([^\"'&]+)", p.srcs)
if t or key:
more = {"api_server": "https://s.uicdn.com/" + MAM + "/"} if MAM in p.srcs else {}
sitekey = attr(t, "data-sitekey") or key
if not sitekey:
return browser("captchafox", "captchafox", p, "no sitekey in the static HTML")
return hit("captchafox", "captchafox", sitekey=sitekey, url=p.url, **more)
def friendly(p): # v2 loads @friendlycaptcha/sdk (site.min.js), v1 loads friendly-challenge
t = tag("frc-captcha", p.h)
if not t:
return None
v2_end, v1_end = attr(t, "data-api-endpoint"), attr(t, "data-puzzle-endpoint")
server = v2_end or v1_end
v2 = v2_end is not None or re.search(r"friendlycaptcha/sdk|site(\.compat)?\.min\.js", p.srcs)
v1 = v1_end is not None or re.search(r"friendly-challenge|widget(\.module)?\.min\.js", p.srcs)
if bool(v2) == bool(v1):
return browser("friendly_captcha", "friendly_captcha", p, "cannot tell v1 from v2 in the HTML")
if not attr(t, "data-sitekey"):
return browser("friendly_captcha", "friendly_captcha", p, "no sitekey in the static HTML")
more = {"api_server": server} if server else {}
return hit("friendly_captcha_" + ("v2" if v2 else "v1"), "friendly_captcha",
sitekey=attr(t, "data-sitekey"), url=p.url, version="v2" if v2 else "v1", **more)У каждого из них есть своя страница решателя с параметрами, которые принимает метод:
- Страница решателя Capy Puzzle: три значения, которые возвращает ответ Capy.
- Страница решателя CaptchaFox: параметры, которые принимает метод CaptchaFox.
- Страница решателя Friendly Captcha: метод Friendly Captcha.
Шаг 8: капча-изображение, предположение
У капчи-изображения нет скрипта поставщика, который можно найти, поэтому эта проверка является предположением и запускается, только если ничего другого не совпало. Проверка ищет изображение, в теге которого есть captcha, и текстовое поле в другом месте страницы, в теге которого есть captcha или security code. Она промахивалась во всех случаях, где в разметке этих слов не было, а нарисованный canvas для неё невидим. Считайте срабатывание вероятным, но не достоверным.
def image(p): # a guess: an <img> that says captcha, next to a text box that says captcha
box = r"<input\b(?![^>]*type\s*=\s*[\"']?(?:hidden|checkbox|submit))[^>]*(?:captcha|security.?code)"
for img in re.findall(r"<img\b[^>]*>", p.h, re.I):
if re.search(box, p.h, re.I) and re.search(r"captcha", img, re.I) and attr(img, "src"):
return hit("image", "normal", image=urljoin(p.url, attr(img, "src")))Шаг 9: проверки, detect и solve
Последний блок перечисляет проверки по порядку и предоставляет detect и solve, а detect_all как вспомогательную функцию. Порядок и есть логика: сначала неподдерживаемые поставщики, затем страница Cloudflare, затем hCaptcha, затем каждое другое семейство, и предположение про изображение только если список ничего не нашёл. Функция detect возвращает первый результат и добавляет список also, если на странице больше одного семейства, например скрипт v3 плюс виджет Turnstile. Список называет семейства, а не второй виджет reCAPTCHA с другим ключом. Функция solve передаёт результат нужному методу CapSkip. Она отказывается обрабатывать всё unsupported или помеченное needs_browser и выбрасывает RuntimeError, а изображение скачивает через вашу собственную Session, чтобы cookies совпадали.
В SDK пока нет вызова для hCaptcha, поэтому результат hCaptcha вместо этого уходит в solve_hcaptcha. Функция отправляет sitekey и адрес страницы в in.php с method=hcaptcha, затем каждые 5 секунд опрашивает res.php, пока токен не будет готов, как описано в разделе hCaptcha документации API на сайте. Передайте адрес решателя как api, а ваш API key как key, если включена проверка API key. Результат несёт токен в code и User-Agent, с которым он получен, в userAgent.
CHECKS = [unsupported, cf_challenge, hcaptcha, recaptcha, turnstile, geetest, altcha, capy, captchafox, friendly]
def detect_all(html, page_url):
p = parse(html, page_url)
return [r for r in (check(p) for check in CHECKS) if r] or [r for r in [image(p)] if r]
def detect(html, page_url):
found = detect_all(html, page_url)
if len(found) > 1:
found[0]["also"] = [r["type"] for r in found[1:]]
return found[0] if found else None
def solve_hcaptcha(args, api, key=None): # no SDK call yet: post to in.php, poll res.php
form = {"method": "hcaptcha", "sitekey": args["sitekey"], "pageurl": args["url"],
"invisible": args.get("invisible", 0), "data": args.get("data"), "key": key, "json": 1}
job = requests.post(api + "/in.php", data=form, timeout=30).json()
if job["status"] != 1:
raise RuntimeError(job["request"])
for _ in range(36): # a solve takes 10 to 30 seconds, so give up after 3 minutes
time.sleep(5)
q = {"key": key, "action": "get", "id": job["request"], "json": 1}
r = requests.get(api + "/res.php", params=q, timeout=30).json()
if r["request"] != "CAPCHA_NOT_READY":
if r["status"] != 1:
raise RuntimeError(r["request"])
return {"code": r["request"], "userAgent": r.get("useragent")}
raise TimeoutError("hCaptcha was not solved within 3 minutes")
def solve(info, solver, session=None, api="http://127.0.0.1:8080", key=None):
if info.get("unsupported") or info.get("needs_browser"):
raise RuntimeError(info.get("needs_browser") or info["type"] + " is not supported by CapSkip")
args = dict(info["args"])
if info["method"] == "hcaptcha":
return solve_hcaptcha(args, api, key)
if info["method"] == "normal": # fetch the image with YOUR session so the cookie matches it
src = args.pop("image")
if not src.startswith("data:"):
r = (session or requests).get(src, timeout=30)
r.raise_for_status()
src = "data:image/png;base64," + base64.b64encode(r.content).decode()
args["file"] = src
return getattr(solver, info["method"])(**args)Запуск
Сохраните это как use.py рядом с detect.py и запустите Python самого окружения, как в .\.venv\Scripts\python.exe use.py. Порт 8080 используется по умолчанию; если у вас другой, возьмите порт из настроек подключения CapSkip. Скрипт загружает вашу страницу, печатает то, что нашёл детектор, и вызывает CapSkip, только когда есть метод для вызова и ничего не пропущено.
import requests
from capskip import CapSkip
from detect import detect, solve
URL = "https://example.com/login" # a page of yours with a CAPTCHA
HOST, PORT = "127.0.0.1", 8080 # from CapSkip's connection settings
session = requests.Session()
html = session.get(URL, timeout=30).text
info = detect(html, URL)
print(info)
if info and info["method"] and not info.get("needs_browser"):
solver = CapSkip(host=HOST, port=PORT)
result = solve(info, solver, session, api=f"http://{HOST}:{PORT}")
print(result)На странице с чекбоксом reCAPTCHA v2 первый вывод показывает тип, метод и аргументы, а второй показывает результат с токеном в поле code. В запуске на имитации это выглядело так:
| Страница | detect возвращает |
|---|---|
| reCAPTCHA v2 checkbox | type recaptcha_v2, method recaptcha, sitekey и url |
| Виджет hCaptcha | type hcaptcha, method hcaptcha, sitekey и url |
| Страница Cloudflare “Just a moment” | type turnstile_challenge, задан needs_browser |
| Скрипт v3 и виджет Turnstile | type recaptcha_v3, в also указан turnstile |
Когда HTML ничего не говорит: консоль браузера
Некоторые страницы добавляют виджет через JavaScript после загрузки. Обычная загрузка тогда не находит капчу вообще, и детектор возвращает None для страницы, на которой она есть. Известны два ожидаемых случая: скрипт Google или Cloudflare, добавленный кодом, и sitekey, заданный кодом. Для них откройте страницу в Chrome, нажмите F12 и вставьте это в Console. Если Chrome блокирует вставку, сначала введите allow pasting и нажмите Enter. Сниппет читает sitekey из ресурсов, которые страница уже загрузила, поэтому видит то, что построил браузер.
(() => {
const iframes = [...document.querySelectorAll('iframe')].map((f) => f.src);
const urls = performance.getEntriesByType('resource').map((r) => r.name).concat(iframes);
const found = new Map(), add = (o) => found.set(JSON.stringify(o), o);
for (const u of urls) {
let m;
if ((m = u.match(/recaptcha\/(?:api2|(enterprise))\/anchor\?.*?\bk=([\w-]+)/)))
add({ type: 'recaptcha', sitekey: m[2], enterprise: !!m[1], invisible: /size=invisible/.test(u) });
else if ((m = u.match(/recaptcha\/(?:api|enterprise)\.js\?.*?\brender=([\w-]+)/)) && m[1] !== 'explicit')
add({ type: 'recaptcha v3', sitekey: m[1], enterprise: /enterprise\.js/.test(u) });
else if ((m = u.match(/challenges\.cloudflare\.com\/.*turnstile\/.*\/(0x4[\w-]+|[123]x0+[A-Za-z]+)\//)))
add({ type: 'turnstile', sitekey: m[1] });
else if ((m = u.match(/hcaptcha\.com\/captcha\/.*[#&?]sitekey=([\w-]+)/))) add({ type: 'hcaptcha', sitekey: m[1] });
else if (/arkoselabs\.com\/fc\//.test(u)) add({ type: 'arkose: CapSkip cannot solve' });
}
if (window._cf_chl_opt) add({ type: 'cloudflare challenge page' });
document.querySelectorAll('altcha-widget').forEach((e) => add({ type: 'altcha', challenge: e.getAttribute('challenge') || e.getAttribute('challengeurl') }));
document.querySelectorAll('.h-captcha[data-sitekey]').forEach((e) => add({ type: 'hcaptcha', sitekey: e.dataset.sitekey }));
document.querySelectorAll('.frc-captcha, .captchafox').forEach((e) => add({ type: e.classList.contains('captchafox') ? 'captchafox' : 'frc-captcha', sitekey: e.dataset.sitekey }));
const v3 = [...found.values()].filter((o) => o.type === 'recaptcha v3').map((o) => o.sitekey); // a v3 badge also adds an invisible anchor frame
const res = [...found.values()].filter((o) => !(o.type === 'recaptcha' && v3.includes(o.sitekey)));
return res.length ? res : 'no known CAPTCHA found (GeeTest, Capy and CaptchaFox scripts are not checked)';
})();Он нашёл sitekey и флаги на странице reCAPTCHA v2, на невидимой, на странице v3, на странице Turnstile и на демо hCaptcha, а на странице без капчи вывел сообщение. Три ограничения: браузер по умолчанию хранит только около 250 записей ресурсов, поэтому долго работающее одностраничное приложение может потерять ранние, и сниппет не отличает Friendly Captcha версии 1 от версии 2, и не ищет скрипты GeeTest, Capy или CaptchaFox. На полностраничной проверке Cloudflare он может показать sitekey из адреса ресурса, но не значения data и pagedata, для которых нужен перехват вызова render. Этот перехват здесь не проверялся.
Где определение даёт сбой
| Ситуация | Что возвращает detect | Что делать |
|---|---|---|
| Скрипт виджета, добавленный через JavaScript | Нет | Загрузите страницу в браузере и запустите сниппет для консоли |
| Sitekey, заданный через JavaScript | needs_browser | Прочитайте ключ с живой страницы |
| Полностраничная проверка Cloudflare | needs_browser | Прочитайте значения в браузере, см. руководство по cf_clearance |
| Скрипт CaptchaFox, Friendly Captcha или Capy без виджета в HTML | Нет | Запустите сниппет для консоли или прочитайте исходный код страницы |
| Страница 403 или 503 от самого сайта | Нет | Сначала выведите код статуса: на странице блокировки нет капчи, которую можно найти |
| Arkose, GeeTest v4 | unsupported | CapSkip не может их решить |
| hCaptcha Enterprise, которая задаёт rqdata | needs_browser | Для каждого задания читайте свежий rqdata с живой страницы и передавайте его в solve_hcaptcha как data |
| Friendly Captcha без подсказки в скрипте | needs_browser | Проверьте, какой скрипт загружает страница |
| Капча-изображение без слова captcha | Нет | Добавьте собственное правило для этого сайта |
Частые ошибки
| Ошибка | Причина | Исправить |
|---|---|---|
| ERROR_GOOGLEKEY | Ключом оказалось слово explicit или ключ, не относящийся к reCAPTCHA | Читайте ключ из виджета, а не из параметра render |
| ERROR_CAPTCHA_UNSOLVABLE | Любое неудавшееся задание выглядит так, в том числе при неверной версии reCAPTCHA, и причина в нём не названа | Сначала проверьте определённый тип, затем откройте раздел Документация API |
| ValidationException | Параметр, которого метод не принимает, например action в вызове v2 | Используйте аргументы, которые вернул детектор |
| NetworkException | CapSkip не запущен либо неверно указаны хост или порт | Запустите CapSkip и проверьте настройки подключения |
| RuntimeError из solve | Результат unsupported или needs_browser, либо CapSkip отклонил задание hCaptcha, и сообщение содержит его код ошибки | Обработайте первые два случая до вызова solve, а для третьего проверьте sitekey и адрес страницы |
Запуск решателя на сервере
Детектору от CapSkip нужны только две вещи: хост и порт. В режиме Local это 127.0.0.1 и ваш собственный ПК. В режиме Server CapSkip слушает ваш сетевой адрес или публичный IP, поэтому детектор, работающий на другом ПК, VPS или размещённой платформе, может обращаться к нему через тот же API. Смените HOST и PORT в начале use.py, и больше в коде ничего не меняется. Для стабильного удалённого доступа рекомендуется статический публичный IP. Режим Server всё равно работает на вашем оборудовании и всё равно без счётчика. Учтите, что режим Server открывает порт для любого адреса, поэтому добавьте в Windows Firewall блокирующее правило для адресов, которым доступ к нему не нужен. Смотрите раздел Настройки подключения с описанием переключения режимов.
FAQ
Можно ли узнать, какую капчу использует сайт, без кода?
Да. Откройте DevTools, перейдите на вкладку Network, перезагрузите страницу и посмотрите на адреса скриптов. Скрипт google.com/recaptcha или recaptcha.net означает reCAPTCHA, challenges.cloudflare.com означает Turnstile, а js.hcaptcha.com означает hCaptcha. Сниппет для Console выше делает тот же поиск за вас.
Почему detect возвращает needs_browser, а не догадку?
Потому что неверная догадка тратит впустую целое решение и даёт токен, который сайт отклоняет без объяснений. Если в статическом HTML нет sitekey, challenge GeeTest или версии Friendly Captcha, лучше так и сказать: это проще, чем разбираться с неудавшимся заданием.
Решает ли CapSkip hCaptcha?
Да. CapSkip решает чекбоксы, невидимые виджеты и виджеты Enterprise hCaptcha через свой HTTP API с method=hcaptcha, как описано в разделе Документация API на сайте. В Python SDK пока нет вызова для hCaptcha, поэтому solve_hcaptcha выше сама отправляет задание в in.php. FunCaptcha, Arkose и GeeTest v4 по-прежнему не поддерживаются, и детектор помечает их как unsupported, чтобы скрипт мог пропустить страницу, а не падать на ней.
Может ли размещённая платформа использовать детектор с моим CapSkip?
Да, через режим Server. Настройте CapSkip на прослушивание вашей сети или публичного IP, укажите этот адрес в HOST и PORT в use.py, и детектор вместе с вызовом решения работают так же, как на одном ПК. Решатель остаётся на вашем оборудовании.
Коротко
Сначала определите тип капчи, затем решайте: читайте скрипт и разметку в фиксированном порядке, передавайте аргументы нужному методу и считайте None, needs_browser и unsupported ответами, а не ошибками. Пока CapSkip работает на вашей машине, счётчик не крутится, пока вы проверяете страницу. Поэтому безлимитный сервис распознавания капчи подходит для краулинга, который каждые несколько минут встречает новый сайт. Сам SDK для Python описан на странице сервиса распознавания капчи для Python.
