""" //input[@name="cf-turnstile-response"] //footer/footer-inner/footer-wrapper/div[@class="ray-id"] iFrame with checkbox
Значок zkillboard.com

zkillboard.com

Выполнение проверки безопасности

Этот веб-сайт использует сервис безопасности для защиты от вредоносных ботов. Эта страница отображается, пока веб-сайт проверяет, что вы не бот.

""" import logging import re import time import traceback from random import randint from patchright.async_api import Page from ..base import Detector, ChallengeInfo, AntiBotProvider, Solver from ..base import Stage from ..registry import register from ..humanize import wait_for_page_stability, wait_for_networkidle, click_human log = logging.getLogger(__name__) class CloudflareDetector(Detector): # степени риска, которые Cloudflare сам проставляет в конфиге своего # челлендж-скрипта на странице (см. `cType: '...'` в html/js) — # по нему и определяем стадию, а не по факту наличия виджета на глаз _CTYPES = ("non-interactive", "managed", "interactive") async def detect(self, page: Page, info: ChallengeInfo): log.debug("CloudflareDetector run") await wait_for_page_stability(page, True, False) log.debug("Страница загружена полностью") info.meta = {} widget_selector = "div.cf-turnstile[data-sitekey], div[data-sitekey].cf-turnstile" marker_selector = info.detect_locator or "xpath=//input[@name='cf-turnstile-response']" info.detect_locator = marker_selector log.debug(f"ищем {marker_selector} успех если есть {info.success_locator}") cf_type = await self._detect_cloudflare(page) if not cf_type: log.debug("на странице нет маркера преграды") if info.success_locator: info.success = bool(await page.query_selector(info.success_locator)) info.meta["reason"] = [ "Нет маркера блокировки и нет маркера успеха :-(", "Нет маркера блокировки зато есть маркер успеха :-)" ][info.success] else: info.success = True info.meta["reason"] = "нет маркера блокировки" log.debug("Решение не требуется") return info.meta["cf_type"] = cf_type log.debug(f"cType со страницы: {cf_type!r}") widget = await page.query_selector(widget_selector) if cf_type == "non-interactive": # фулл-пейдж интерстишл без чекбокса — просто ждать, пока пропадёт # или эскалирует в managed/interactive (тогда cf_type сменится на # следующем detect()) info.stage = Stage.INTERSTITIAL elif cf_type in ("managed", "interactive"): # CF уже решил, что нужен клик по виджету внутри своего iframe # (challenges.cloudflare.com) — сам клик этот проект не решает info.stage = Stage.CHECKBOX info.meta["requires_click"] = True elif widget: # обычный встроенный виджет на странице (без cType, просто #
) — решаем через # captcha_solver по токену, см. CloudflareSolver info.stage = Stage.CHECKBOX info.meta["container_selector"] = widget_selector else: info.stage = Stage.INTERSTITIAL @staticmethod async def _detect_cloudflare(page: Page) -> str | None: """ Detect the type of Cloudflare challenge present in the provided page content. This function analyzes the given page content to identify whether a specific type of Cloudflare challenge is present. It checks for three predefined challenge types: non-interactive, managed, and interactive. If a challenge type is detected, it returns the corresponding type as a string. If no challenge type is detected, it returns None. Args: page_content (str): The content of the page to analyze for Cloudflare challenge types. Returns: str: A string representing the detected Cloudflare challenge type, if found. Returns None if no challenge matches. """ challenge_types = ( "non-interactive", "managed", "interactive", ) for _ in range(2): try: page_content = await page.content() except Exception: traceback.print_exc() await wait_for_page_stability(page, True, True) continue else: break for ctype in challenge_types: if f"cType: '{ctype}'" in page_content: return ctype # Check if turnstile captcha is embedded inside the page (Usually inside a closed Shadow iframe) if await page.query_selector('script[src*="challenges.cloudflare.com/turnstile/v"]'): return "embedded" return None __CF_PATTERN__ = re.compile(r"^https://challenges\.cloudflare\.com/cdn-cgi/challenge-platform/.*") class CloudflareSolver(Solver): # опрос исчезновения маркера челленджа после клика / на interstitial POLL_INTERVAL_MS = 250 WAIT_CLEARED_S = 10 async def solve(self, page: Page, info: ChallengeInfo): log.debug("start solving") iframe = page.frame(url=__CF_PATTERN__) if iframe: await page.wait_for_timeout(randint(500, 1000)) frame_el = await iframe.frame_element() box = await frame_el.bounding_box() else: container = await page.query_selector("xpath=//input[@name='cf-turnstile-response']/../div") box = await container.bounding_box() if container else None if info.stage == Stage.CHECKBOX: if box is None: raise RuntimeError("turnstile widget not found on the page") # # Calculate the Captcha coordinates for any viewport captcha_x, captcha_y = box["x"] + randint(26, 28), box["y"] + randint(25, 27) await click_human(page, captcha_x, captcha_y) await wait_for_networkidle(page) if info.success_locator: try: await page.wait_for_selector(info.success_locator) except: pass finally: return else: deadline = time.time() + self.WAIT_CLEARED_S while time.time() < deadline: if not await page.query_selector(info.detect_locator): return await page.wait_for_timeout(self.POLL_INTERVAL_MS) return elif info.stage == Stage.INTERSTITIAL: if info.success_locator: try: await page.wait_for_selector(info.success_locator) except Exception: return else: return else: deadline = time.time() + self.WAIT_CLEARED_S while time.time() < deadline: if not await page.query_selector(info.detect_locator): return await page.wait_for_timeout(self.POLL_INTERVAL_MS) return else: captcha_solver = self._opt.get("captcha_solver") if not captcha_solver: raise RuntimeError("Нужен captcha_solver (2captcha) чтобы решить Turnstile") await captcha_solver.solve(info.meta["container_selector"], page) # TODO: возбудить js @classmethod def _challenge_cleared(cls, page_content: str, challenge_type: str) -> bool: """ Check whether the Cloudflare challenge is no longer present in the page content. Args: page_content (str): The content of the page to analyze. challenge_type (str): The challenge type returned by `_detect_cloudflare`. Returns: bool: True if the challenge is gone from the page, False otherwise. """ return challenge_type == "embedded" or cls._detect_cloudflare(page_content) is None @register class CloudflareProvider(AntiBotProvider): provider = 'cloudflare' _solver_cls = CloudflareSolver _detector_cls = CloudflareDetector