Эта статья была полезной?
Как обойти Cloudflare Challenge с помощью Playwright
Технический специалист
Cloudflare Challenge — это не капча внутри формы, а полная подмена целевой страницы. Вместо контента браузер получает интерстишиал «Verifying you are human», и только после успешной проверки Cloudflare пропускает его дальше. Для живого пользователя это чаще всего занимает пару секунд и проходит незаметно. Для Playwright-скрипта это тупик: page.goto() отрабатывает без ошибок, а в DOM — ни одного нужного селектора.
В этом руководстве разберём, как пройти Challenge из Playwright с помощью API RuCaptcha: перехватить динамические параметры виджета до его инициализации, отправить их на решение и передать полученный токен в callback-функцию страницы.
Challenge и standalone Turnstile — не одно и то же
Эти два кейса часто путают, а решаются они по-разному.
| Standalone Turnstile | Cloudflare Challenge | |
|---|---|---|
| Где находится | Виджет .cf-turnstile внутри формы на обычной странице |
Отдельная страница-интерстишиал вместо целевого контента |
| Параметры задачи | websiteURL, websiteKey |
websiteURL, websiteKey + action, data, pagedata, userAgent |
| Где взять sitekey | Атрибут data-sitekey в DOM |
Только перехватом вызова turnstile.render |
| Что делать с токеном | Записать в input[name="cf-turnstile-response"] или вызвать callback |
Только вызвать callback |
Если у вас первый случай — вам нужен туториал Как обойти Cloudflare Turnstile с помощью Playwright. Здесь речь идёт о втором.
Главное отличие в том, что на Challenge-странице параметры cData, chlPageData и action генерируются заново при каждой загрузке, живут считанные секунды и нигде не лежат в разметке — они передаются аргументом в вызов turnstile.render(). Значит, единственный способ их получить — подменить сам turnstile.render до того, как Cloudflare его вызовет.
Что понадобится
- Python 3.8+
- Playwright с установленным Chromium
- Аккаунт RuCaptcha и API-ключ (получить здесь)
- URL целевой страницы, защищённой Cloudflare. Чтобы обкатать скрипт, удобно взять демо-страницу Cloudflare Challenge — примеры ниже написаны под неё
- Прокси — если целевой сайт чувствителен к репутации IP (подробнее в шаге 4)
Шаг 1. Установка зависимостей
bash
pip install playwright 2captcha-python
playwright install chromium
Шаг 2. Запуск браузера и подготовка перехвата
Перехватчик обязан оказаться в странице раньше, чем загрузится скрипт Turnstile. Это не рекомендация, а условие работоспособности: turnstile.render() вызывается один раз, и если к этому моменту вы его не подменили, перехватывать уже нечего — параметры уйдут внутрь виджета и останутся недоступны. Именно на этом ломается большинство скриптов: код внедряют через page.evaluate() после page.goto() и стабильно опаздывают.
В Playwright для этого есть штатный механизм — add_init_script(). Он выполняет ваш код в каждом новом документе до запуска любых скриптов страницы, включая скрипты Cloudflare.
Начнём с контекста браузера. На этапе отладки держите headless=False — так видно, что реально происходит со страницей.
python
from playwright.sync_api import sync_playwright
USER_AGENT = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
context = browser.new_context(user_agent=USER_AGENT, locale="en-US")
page = context.new_page()
User-Agent задаём явно и запоминаем: строка, которую вы передаёте в задачу на решение, должна совпадать с той, под которой браузер открывает сайт. Cloudflare учитывает User-Agent при оценке запроса, и расхождение делает токен бесполезным.
Шаг 3. Перехват параметров Challenge
Внедряем init-скрипт. Он ждёт появления объекта window.turnstile, подменяет его метод render, забирает аргументы вызова и сохраняет callback в глобальной переменной window.tsCallback — именно этот callback позже примет токен.
python
INTERCEPT_SCRIPT = """
const i = setInterval(() => {
if (window.turnstile) {
clearInterval(i);
window.turnstile.render = (a, b) => {
const params = {
websiteKey: b.sitekey,
websiteURL: window.location.href,
data: b.cData,
pagedata: b.chlPageData,
action: b.action,
userAgent: navigator.userAgent
};
window.tsCallback = b.callback;
window.onChallengeParams(JSON.stringify(params));
return;
};
}
}, 50);
"""
Оригинальный render вызывается кодом Cloudflare и получает два аргумента: селектор контейнера и объект настроек. Нас интересует второй — из него мы забираем sitekey, cData, chlPageData и action, а заодно сохраняем ссылку на callback. Виджет при этом не рендерится: подменённая функция просто выходит.
Теперь нужно передать перехваченные параметры из контекста страницы в Python. page.expose_function() пробрасывает в страницу настоящую Python-функцию, которую JS может вызвать напрямую — это надёжнее, чем разбирать сообщения консоли:
python
import json
challenge_params = {}
def on_challenge_params(raw):
challenge_params.update(json.loads(raw))
print("Параметры перехвачены:", challenge_params["action"])
page.expose_function("onChallengeParams", on_challenge_params)
page.add_init_script(INTERCEPT_SCRIPT)
page.goto(TARGET_URL)
page.wait_for_function("() => window.tsCallback !== undefined", timeout=30000)
Порядок вызовов важен: expose_function и add_init_script идут до goto, иначе смысл теряется.
wait_for_function здесь надёжнее, чем time.sleep(): скрипт продолжится ровно в тот момент, когда Cloudflare реально вызвал render и callback стал доступен.
Важно: с этого момента и до отправки токена страницу нельзя перезагружать, а вкладку — закрывать.
cDataиchlPageDataодноразовые и привязаны к конкретной загрузке документа. Перезагрузили — все перехваченные значения превратились в мусор, и API вернётERROR_CAPTCHA_UNSOLVABLE.
Шаг 4. Отправка задачи на решение
Для Challenge используется тот же тип задачи, что и для обычного Turnstile, но с дополнительными полями. Полный запрос выглядит так:
json
{
"clientKey": "YOUR_API_KEY",
"task": {
"type": "TurnstileTaskProxyless",
"websiteURL": "https://rucaptcha.com/demo/cloudflare-turnstile-challenge",
"websiteKey": "0x4AAAAAAAAjq6WYeRDKmebM",
"action": "managed",
"data": "80001aa1affffc21",
"pagedata": "3gAFo2l...55NDFPRFE9",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..."
}
}
Есть два типа задач:
- TurnstileTaskProxyless — решение выполняется через пул прокси сервиса. Подходит, если целевой сайт не привязывает результат к IP.
- TurnstileTask — решение выполняется через ваш прокси. Дополнительно передаются
proxyType,proxyAddress,proxyPort,proxyLogin,proxyPassword. Используйте этот вариант, если Challenge выдаётся агрессивно или сайт строго сверяет IP.
Практическое правило: начните с proxyless. Если токен стабильно не принимается — переключайтесь наTurnstileTaskи передавайте тот же прокси, через который работает сам Playwright.
Через официальный SDK создание задачи и опрос результата укладываются в один вызов:
python
from twocaptcha import TwoCaptcha
solver = TwoCaptcha("YOUR_API_KEY", server="rucaptcha.com")
result = solver.turnstile(
sitekey=challenge_params["websiteKey"],
url=challenge_params["websiteURL"],
action=challenge_params["action"],
data=challenge_params["data"],
pagedata=challenge_params["pagedata"],
useragent=challenge_params["userAgent"],
)
token = result["code"]
Два момента, на которых легко потерять час отладки:
server="rucaptcha.com"обязателен. По умолчанию конструкторTwoCaptchaшлёт запросы на2captcha.com, а аккаунты RuCaptcha и 2Captcha не общие — без этого параметра вы получите ошибку ключа.- Параметр называется
useragent, одним словом — неuser_agent. Написание важно: дополнительные аргументы уходят в API как есть, поэтому опечатка не вызовет ошибку. User-Agent просто не дойдёт до решателя, и токен окажется нерабочим.
Решение занимает обычно 10–25 секунд.
Шаг 5. Внедрение токена через callback
Здесь Challenge принципиально отличается от standalone-виджета. Записать токен в скрытый input недостаточно и, как правило, просто бессмысленно: на интерстишиале нет формы, которую вы отправляете. Значение токена должен получить именно callback — он передаёт результат проверки Cloudflare, после чего браузер попадает на исходную страницу.
Мы сохранили этот callback в window.tsCallback ещё на шаге 3, поэтому вызов сводится к одной строке:
python
page.evaluate("(token) => window.tsCallback(token)", token)
Дальше остаётся дождаться, пока Cloudflare отпустит браузер. Ориентируйтесь не на таймаут, а на появление целевого контента:
python
page.wait_for_load_state("networkidle")
page.wait_for_selector("#content", timeout=30000)
Селектор подставьте свой — любой элемент, которого на странице проверки заведомо нет.
Полный рабочий пример
python
import json
from playwright.sync_api import sync_playwright
from twocaptcha import TwoCaptcha
API_KEY = "YOUR_API_KEY"
TARGET_URL = "https://rucaptcha.com/demo/cloudflare-turnstile-challenge"
USER_AGENT = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
INTERCEPT_SCRIPT = """
const i = setInterval(() => {
if (window.turnstile) {
clearInterval(i);
window.turnstile.render = (a, b) => {
const params = {
websiteKey: b.sitekey,
websiteURL: window.location.href,
data: b.cData,
pagedata: b.chlPageData,
action: b.action,
userAgent: navigator.userAgent
};
window.tsCallback = b.callback;
window.onChallengeParams(JSON.stringify(params));
return;
};
}
}, 50);
"""
solver = TwoCaptcha(API_KEY, server="rucaptcha.com")
params = {}
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
context = browser.new_context(user_agent=USER_AGENT, locale="en-US")
page = context.new_page()
page.expose_function("onChallengeParams", lambda raw: params.update(json.loads(raw)))
page.add_init_script(INTERCEPT_SCRIPT)
page.goto(TARGET_URL)
page.wait_for_function("() => window.tsCallback !== undefined", timeout=30000)
print("Параметры перехвачены:", params.get("action"))
result = solver.turnstile(
sitekey=params["websiteKey"],
url=params["websiteURL"],
action=params.get("action"),
data=params.get("data"),
pagedata=params.get("pagedata"),
useragent=params["userAgent"],
)
token = result["code"]
print("Токен получен")
page.evaluate("(token) => window.tsCallback(token)", token)
page.wait_for_load_state("networkidle")
print("Текущий URL:", page.url)
browser.close()
Отчёты о точности решения
Если сайт отклонил токен, отправьте reportIncorrect — это возвращает средства за неудачную попытку и помогает сервису улучшать алгоритмы. При успешном прохождении отправляйте reportCorrect.
python
import requests
API_KEY = "YOUR_API_KEY"
TASK_ID = "123456789"
requests.post("https://api.rucaptcha.com/reportIncorrect",
json={"clientKey": API_KEY, "taskId": TASK_ID})
requests.post("https://api.rucaptcha.com/reportCorrect",
json={"clientKey": API_KEY, "taskId": TASK_ID})
Подробный разбор с примерами для разных языков, лимитами и разбором ошибок ERROR_REPORT_NOT_RECORDED и ERROR_DUPLICATE_REPORT — в гайде Как отправлять отчёты.
Частые ошибки и решения
| Проблема | Причина | Решение |
|---|---|---|
| Параметры перехватываются пустыми | Скрипт внедрён после инициализации Turnstile | Используйте add_init_script() до goto(), а не evaluate() после |
ERROR_CAPTCHA_UNSOLVABLE |
data / pagedata устарели или пустые |
Не перезагружайте страницу между перехватом и отправкой. Отправляйте задачу сразу после срабатывания expose_function |
| Токен получен, но страница не меняется | Callback не вызван или вызван не тот | Вызывайте именно сохранённый window.tsCallback(token) |
Работает при headless=False, падает при True |
Cloudflare учитывает фингерпринт браузера | Добавьте playwright-extra со stealth-плагином, реалистичный UA и резидентный прокси |
| Токен отклоняется при верном наборе параметров | Расхождение User-Agent между браузером и задачей | Используйте одну и ту же строку UA в new_context() и в параметре useragent |
ERROR_WRONG_USER_KEY |
Неверный API-ключ либо не указан server="rucaptcha.com" |
Проверьте ключ в личном кабинете и адрес сервера в конструкторе |
| Ошибка поиска элементов после прохождения | Сайт обновил вёрстку | Актуализируйте селекторы через DevTools, используйте expect-ожидания Playwright |