- слоты браузеров: asyncio.Semaphore, размер из BROWSER_SLOTS, при нехватке — 503 + Retry-After; устранена утечка счётчика при отказе (#12, #49) - session_id принимается только как UUID, путь профиля проверяется (#5) - секреты клиента в логах маскируются, полный дамп — только на DEBUG (#6) - ответ: status всегда "ok"/"err", поле code (no_slots / request_failed / challenge_failed / action_failed), response_model (#7) - непройденный антибот → challenge_failed, действия не выполняются (#8) - таймауты провайдеров в мс вынесены в константы; detect переживает навигацию посреди проверки; пауза между неудачными solve вместо холостого цикла (#9) - обязательные поля действий и формат прокси проверяются схемой → 422 до запуска браузера (#13) - прокси необязателен (#15) - user_agent сохраняется в профиле сессии и реально передаётся в браузер (#16) - duration_ms у действий и у запроса (#18), опция screenshot (#19) - общий конфиг логирования (LOG_LEVEL), print → логгер, CloudflareProvider, удалён cached_persistent, GET /health (#22) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
43 lines
1.3 KiB
Python
43 lines
1.3 KiB
Python
"""
|
|
|
|
<div id="cf-turnstile" class="cf-turnstile" data-sitekey="3x00000000000000000000FF">
|
|
<div>
|
|
<div></div>
|
|
<input type="hidden" name="cf-turnstile-response" id="cf-chl-widget-a3ptc_response">
|
|
</div>
|
|
</div>
|
|
|
|
Кликабельный чекбокс Cloudflare рендерит в закрытом shadow DOM — туда лезть
|
|
не нужно и бессмысленно. sitekey и поле для ответа лежат в обычном DOM
|
|
контейнера, поэтому решаем через API 2captcha и просто подставляем токен.
|
|
"""
|
|
import logging
|
|
|
|
from .base import Captcha, Page
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SolverTurnstile(Captcha):
|
|
ctype = "cloudflare"
|
|
|
|
async def solve(self, selector, page: Page):
|
|
container = page.locator(selector)
|
|
await container.wait_for()
|
|
|
|
site_key = await container.get_attribute("data-sitekey")
|
|
|
|
response_input = container.locator('input[name="cf-turnstile-response"]')
|
|
await response_input.wait_for(state="attached")
|
|
|
|
code = await self.turnstile(page.url, site_key)
|
|
|
|
await response_input.evaluate((
|
|
"(el, value) => {"
|
|
"el.value = value;"
|
|
"el.dispatchEvent(new Event('input', {bubbles: true}));"
|
|
"el.dispatchEvent(new Event('change', {bubbles: true}));"
|
|
"}"
|
|
), code)
|
|
return code
|