Если сценарий падает через раз на одной и той же странице — скорее всего, вы жёстко зашили один метод решения, а сайт отдаёт то reCAPTCHA v2, то Cloudflare Turnstile. Лечится это не подбором тайм-аутов, а одним архитектурным решением: определять провайдера по разметке при каждом запросе страницы и уже потом выбирать метод API и имя поля для токена. Ниже — маркеры в разметке, по которым тип узнаётся надёжно, готовый код на Python и JavaScript и разбор ошибок, из-за которых корректно решённый токен всё равно отклоняется формой.
Почему на одном домене оказываются два провайдера
Это не редкость и почти никогда не ошибка вёрстки. Типичные причины:
| Сценарий | Как это выглядит на практике |
|---|---|
| Разные страницы — разные провайдеры | Логин закрыт reCAPTCHA, оформление заказа — Cloudflare Turnstile |
| A/B-тест провайдеров | Один и тот же URL показывает случайный из двух типов |
| Незавершённая миграция | Старые разделы на reCAPTCHA, новые уже на Cloudflare Turnstile |
| Запасной вариант при сбое | Основной провайдер не отвечает → подключается второй |
| Региональные различия | reCAPTCHA для одних регионов, Cloudflare Turnstile для других (соображения GDPR) |
Практический вывод один: провайдер — это свойство конкретного HTTP-ответа, а не свойство сайта. Кэшировать его между запросами нельзя.
Локальный пример: один сценарий на несколько площадок
Типичная задача для команды из Москвы, Минска или Алматы — один парсер собирает публичные каталоги сразу нескольких региональных площадок. Часть из них уже переехала на Cloudflare Turnstile ради более быстрой проверки и меньшего числа сторонних запросов, часть осталась на reCAPTCHA v2. Держать по копии сценария под каждую площадку — тупик: любую правку приходится вносить дважды, а при следующей миграции всё ломается снова. Один сценарий с автоопределением типа снимает эту работу целиком.
Отдельная сноска для этого рынка: если по пути вы сохраняете какие-либо пользовательские данные, собирайте только то, что вы вправе обрабатывать — требования 152-ФЗ «О персональных данных» и GDPR-подобная дисциплина здесь работают на вас. Это ответственность читателя, а не свойство сервиса.
Как определить провайдера по разметке
Различить типы можно по трём независимым признакам: CSS-классу контейнера, адресу подключаемого скрипта и имени поля, куда форма ждёт токен.
| Провайдер | HTML-маркер | URL-адрес скрипта | Поле ответа |
|---|---|---|---|
| reCAPTCHA v2 | class="g-recaptcha" |
google.com/recaptcha/api.js |
g-recaptcha-response |
| Cloudflare Turnstile | class="cf-turnstile" |
challenges.cloudflare.com/turnstile |
cf-turnstile-response |
| hCaptcha | class="h-captcha" |
js.hcaptcha.com/1/api.js |
h-captcha-response |
Строка hCaptcha здесь справочная: по этим маркерам вы поймёте, что перед вами именно hCaptcha. Решение hCaptcha в CaptchaAI не поддерживается, поэтому такую страницу сценарий должен корректно пропустить, а не отправлять задачу в API.
Ключевой момент — порядок проверок. Атрибут data-sitekey есть у всех трёх провайдеров, поэтому искать надо сначала классоспецифичный маркер и только потом ключ. Обратный порядок даёт самую частую ошибку: Turnstile опознаётся как reCAPTCHA, метод API выбирается формально валидный, но не тот.
Python: определение типа и решение в одном проходе
Функция detect_captcha_type() возвращает структуру с провайдером, методом API, значением sitekey и именем поля ответа; solve_captcha() собирает параметры под нужный метод и опрашивает res.php до готовности результата. Обратите внимание: для reCAPTCHA ключ сайта передаётся как googlekey, а для Turnstile — как sitekey. Это разные имена параметров у одного и того же эндпоинта.
import requests
import time
import re
from dataclasses import dataclass
API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
@dataclass
class CaptchaInfo:
provider: str # "recaptcha" or "turnstile"
method: str # API method name
sitekey: str
pageurl: str
response_field: str # Form field name for the token
def detect_captcha_type(html, pageurl):
"""
Detect which CAPTCHA provider is on the page.
Returns CaptchaInfo or None.
"""
# Check for Turnstile
turnstile_match = re.search(
r'class=["\'][^"\']*cf-turnstile[^"\']*["\'][^>]*data-sitekey=["\']([^"\']+)["\']',
html,
)
if not turnstile_match:
turnstile_match = re.search(
r'data-sitekey=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*cf-turnstile',
html,
)
if turnstile_match:
return CaptchaInfo(
provider="turnstile",
method="turnstile",
sitekey=turnstile_match.group(1),
pageurl=pageurl,
response_field="cf-turnstile-response",
)
# Check for reCAPTCHA
recaptcha_match = re.search(
r'class=["\'][^"\']*g-recaptcha[^"\']*["\'][^>]*data-sitekey=["\']([^"\']+)["\']',
html,
)
if not recaptcha_match:
recaptcha_match = re.search(
r'data-sitekey=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*g-recaptcha',
html,
)
# Also check for script-rendered reCAPTCHA
if not recaptcha_match:
recaptcha_match = re.search(
r'grecaptcha\.render\([^,]+,\s*\{[^}]*["\']sitekey["\']\s*:\s*["\']([^"\']+)["\']',
html,
)
if recaptcha_match:
return CaptchaInfo(
provider="recaptcha",
method="userrecaptcha",
sitekey=recaptcha_match.group(1),
pageurl=pageurl,
response_field="g-recaptcha-response",
)
return None
def solve_captcha(info):
"""Solve any detected CAPTCHA type via CaptchaAI."""
params = {
"key": API_KEY,
"method": info.method,
"json": 1,
}
if info.method == "userrecaptcha":
params["googlekey"] = info.sitekey
params["pageurl"] = info.pageurl
elif info.method == "turnstile":
params["sitekey"] = info.sitekey
params["pageurl"] = info.pageurl
resp = requests.post(SUBMIT_URL, data=params, timeout=30).json()
if resp.get("status") != 1:
raise RuntimeError(f"Submit failed: {resp.get('request')}")
task_id = resp["request"]
for _ in range(60):
time.sleep(5)
poll = requests.get(RESULT_URL, params={
"key": API_KEY, "action": "get",
"id": task_id, "json": 1,
}, timeout=15).json()
if poll.get("request") == "CAPCHA_NOT_READY":
continue
if poll.get("status") == 1:
return poll["request"]
raise RuntimeError(f"Solve failed: {poll.get('request')}")
raise RuntimeError("Timeout")
def process_page(session, url):
"""Fetch page, detect CAPTCHA type, solve, and return form-ready data."""
response = session.get(url)
captcha_info = detect_captcha_type(response.text, url)
if not captcha_info:
print(f"No CAPTCHA detected on {url}")
return None
print(f"Detected {captcha_info.provider} on {url}")
print(f" Sitekey: {captcha_info.sitekey[:30]}...")
token = solve_captcha(captcha_info)
print(f" Solved: {token[:30]}...")
return {
"provider": captcha_info.provider,
"response_field": captcha_info.response_field,
"token": token,
}
# Usage: Handle multiple pages with different providers
session = requests.Session()
pages = [
"https://staging.example.com/qa-login", # Might have reCAPTCHA
"https://example.com/checkout", # Might have Turnstile
]
for url in pages:
result = process_page(session, url)
if result:
form_data = {result["response_field"]: result["token"]}
# Add other form fields...
# session.post(url, data=form_data)
Отдельный запасной путь внутри функции определения — регулярное выражение по вызову grecaptcha.render(). Оно нужно для страниц, где виджет создаётся скриптом уже после загрузки и в исходном HTML нет ни класса, ни атрибута data-sitekey.
JavaScript: та же логика в Node.js
Если пайплайн уже написан на Node.js, переносить его на Python ради этой задачи незачем — логика повторяется один в один: сначала маркер Turnstile, затем маркер reCAPTCHA, затем разбор скриптовой отрисовки.
const API_KEY = "YOUR_API_KEY";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";
function detectCaptchaType(html, pageurl) {
// Turnstile
const turnstileMatch = html.match(/cf-turnstile[^>]*data-sitekey=["']([^"']+)["']/);
if (turnstileMatch) {
return { provider: "turnstile", method: "turnstile", sitekey: turnstileMatch[1], pageurl, field: "cf-turnstile-response" };
}
// reCAPTCHA
const recaptchaMatch = html.match(/g-recaptcha[^>]*data-sitekey=["']([^"']+)["']/);
if (recaptchaMatch) {
return { provider: "recaptcha", method: "userrecaptcha", sitekey: recaptchaMatch[1], pageurl, field: "g-recaptcha-response" };
}
// Script-rendered reCAPTCHA
const scriptMatch = html.match(/sitekey["']\s*:\s*["']([^"']+)["']/);
if (scriptMatch) {
return { provider: "recaptcha", method: "userrecaptcha", sitekey: scriptMatch[1], pageurl, field: "g-recaptcha-response" };
}
return null;
}
async function solveCaptcha(info) {
const body = new URLSearchParams({ key: API_KEY, method: info.method, json: "1" });
if (info.method === "userrecaptcha") { body.set("googlekey", info.sitekey); body.set("pageurl", info.pageurl); }
else if (info.method === "turnstile") { body.set("sitekey", info.sitekey); body.set("pageurl", info.pageurl); }
const resp = await (await fetch(SUBMIT_URL, { method: "POST", body })).json();
if (resp.status !== 1) throw new Error(`Submit: ${resp.request}`);
const taskId = resp.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const url = `${RESULT_URL}?key=${API_KEY}&action=get&id=${taskId}&json=1`;
const poll = await (await fetch(url)).json();
if (poll.request === "CAPCHA_NOT_READY") continue;
if (poll.status === 1) return poll.request;
throw new Error(`Solve: ${poll.request}`);
}
throw new Error("Timeout");
}
async function processPage(url) {
const response = await fetch(url);
const html = await response.text();
const info = detectCaptchaType(html, url);
if (!info) { console.log(`No CAPTCHA on ${url}`); return null; }
console.log(`${info.provider} detected on ${url}`);
const token = await solveCaptcha(info);
return { provider: info.provider, field: info.field, token };
}
// Usage
const pages = ["https://staging.example.com/qa-login", "https://example.com/checkout"];
for (const url of pages) {
const result = await processPage(url);
if (result) {
console.log(`Solved ${result.provider}: ${result.token.substring(0, 30)}...`);
}
}
Что ломается чаще всего
| Проблема | Причина | Что делать |
|---|---|---|
| Определился не тот тип | Регулярное выражение зацепилось за data-sitekey чужого виджета |
Сначала проверяйте классы cf-turnstile и g-recaptcha, и только потом ключ |
| Токен получен, но форма его отклоняет | Использовано имя поля от другого провайдера | Поле должно совпадать с типом: g-recaptcha-response против cf-turnstile-response |
| Тип меняется между запусками | A/B-тест или выбор по региону | Определяйте провайдера при каждом запросе страницы, не кэшируйте результат первого визита |
| На странице найдены оба виджета | Один из них скрыт или неактивен | Проверьте видимость элемента и непустой data-sitekey — решать нужно только активный |
| Ничего не найдено, хотя проверка видна | Виджет отрисован скриптом | Ищите вызовы grecaptcha.render() или turnstile.render() в теле страницы |
| Задача уходит в тайм-аут на Turnstile | Ожидание рассчитано по профилю reCAPTCHA | Ориентируйтесь на тип: Cloudflare Turnstile обычно решается менее чем за 10 с, reCAPTCHA v2 — менее чем за 60 с |
Последняя строка стоит отдельного пояснения. Смешанный сайт легко приводит к тому, что общий тайм-аут выставляется по одному типу, а второй в него не укладывается или, наоборот, простаивает. Разумнее держать два разных лимита ожидания и выбирать нужный по тому же полю provider, что вернул детект.
Сколько потоков заложить
Тарификация CaptchaAI считается по одновременным потокам, а не по числу решений: количество решений внутри потока не ограничено. Для смешанного сайта планируйте по самому медленному из встречающихся типов — обычно это reCAPTCHA v2. Тарифа BASIC ($15/мес, 5 потоков) хватает для отладки и небольшого регулярного сценария; при параллельной обработке десятков страниц разумнее смотреть в сторону ADVANCE ($90/мес, 50 потоков). Цены указаны в USD; актуальный список тарифов смотрите на сайте CaptchaAI.
FAQ
Как понять, что на странице Turnstile, а не reCAPTCHA?
По классу контейнера и адресу скрипта: cf-turnstile и challenges.cloudflare.com означают Cloudflare Turnstile, g-recaptcha и google.com/recaptcha/api.js — reCAPTCHA. Смотреть в первую очередь на data-sitekey нельзя: этот атрибут есть у обоих.
Нужны ли разные API-ключи под reCAPTCHA и Turnstile?
Нет. Ключ один, эндпоинты те же — in.php и res.php. Различаются только значение method (userrecaptcha против turnstile) и имя параметра для ключа сайта (googlekey против sitekey).
Что делать, если тип поменялся посреди сессии?
Перезапросить страницу и определить провайдера заново. Так бывает при переключении на резервный провайдер после сбоя основного — и именно поэтому детект должен выполняться на каждом ответе, а не один раз при старте.
Что делать, если на том же домене попалась hCaptcha?
Пропустить такую страницу: hCaptcha и FunCaptcha не входят в число поддерживаемых типов. Поддерживаются reCAPTCHA v2 и v3, Cloudflare Turnstile и Cloudflare Challenge, GeeTest v3, image/OCR, grid и BLS; CaptchaFox (beta), Friendly Captcha (beta) и Lemin (beta) доступны в бета-режиме. GeeTest v4 заявлен как «скоро».
Как выбрать длительность опроса результата?
Отталкивайтесь от типа, а не от одного общего числа. Для Cloudflare Turnstile разумно ждать заметно меньше, чем для reCAPTCHA v2. Интервал в 5 с между опросами res.php и общий лимит попыток, как в примерах выше, покрывают оба случая без лишней нагрузки на API.