From ada64a68ad976322f0b587e00f262b762a591b67 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Tue, 9 Jun 2026 10:13:28 +0800 Subject: [PATCH] =?UTF-8?q?cf=E9=80=BB=E8=BE=91=E5=A4=8D=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/config.py | 1 + app/services/cloudflare_session.py | 47 +++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/app/core/config.py b/app/core/config.py index 50734b1..c42f4bc 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -58,6 +58,7 @@ class Settings(BaseSettings): page_acquire_timeout_seconds: float = 60.0 # 获取页面槽位超时 request_timeout_seconds: float = 120.0 # 页面请求超时 cloudflare_wait_timeout_seconds: float = 60.0 # 等待 Cloudflare 挑战超时 + cloudflare_reuse_wait_timeout_seconds: float = 8.0 # 复用会话时消化挑战的短超时,超时则放弃复用直接重建 session_ttl_seconds: int = 900 # 会话过期时间(秒) browser_locale: str = "ja-JP" browser_timezone_id: str = "Asia/Tokyo" diff --git a/app/services/cloudflare_session.py b/app/services/cloudflare_session.py index 751e087..63dd8e5 100644 --- a/app/services/cloudflare_session.py +++ b/app/services/cloudflare_session.py @@ -119,9 +119,12 @@ class CloudflareSessionManager: """带已有会话导航并消化 Cloudflare 挑战 与新建路径保持一致:导航后等待挑战自动通过,而非一遇非 200 就判死。 + CF 对携带过期 cf_clearance 的请求常直接返回挑战页(首响应可能非 200), + 因此即便首响应非 200,也先给页面一次自动通过 Turnstile 的机会, + 仅当挑战超时/被硬阻断、或通过后仍无有效内容时才判定会话失效。 Returns: - 通过验证的页面 HTML;若判定会话已失效(非 200 或挑战/阻断),返回 None + 通过验证的页面 HTML;若判定会话已失效,返回 None """ async with self._browser_pool.page_session(storage_state=session.storage_state) as (_, page): response = await page.goto( @@ -130,16 +133,28 @@ class CloudflareSessionManager: timeout=int(self._settings.request_timeout_seconds * 1000), ) status = response.status if response is not None else None - if status != 200: - # 携带已有会话仍被判非 200,通常意味着 cf_clearance 已失效,交由上层重建 - logger.debug("复用会话导航返回非 200:url=%s status=%s", target_url, status) - return None try: - await self._wait_for_clearance(page) + # 复用使用短超时:快速验证能否通过,不行就放弃复用交由上层重建, + # 避免在无法自动通过的挑战上空等整个 cloudflare_wait_timeout_seconds。 + await self._wait_for_clearance( + page, timeout_seconds=self._settings.cloudflare_reuse_wait_timeout_seconds + ) except (CloudflareChallengeError, UpstreamBlockedError): - logger.debug("复用会话仍处于挑战/阻断状态,判定会话失效:url=%s", target_url) + logger.info( + "复用会话未通过挑战,判定失效:url=%s status=%s had_cf_clearance=%s", + target_url, status, bool(session.cf_clearance), + ) return None - return await page.content() + + html = await page.content() + # 挑战已通过但页面仍是挑战壳(极少数 wait 提前返回的情况):判失效,触发重建 + if self._looks_like_challenge((await page.title()).lower(), html.lower()): + logger.info( + "复用会话通过等待后仍无有效内容,判定失效:url=%s status=%s had_cf_clearance=%s", + target_url, status, bool(session.cf_clearance), + ) + return None + return html async def invalidate(self, target_url: str) -> None: """主动让会话失效 @@ -203,7 +218,7 @@ class CloudflareSessionManager: html=html_content, ) - async def _wait_for_clearance(self, page: object) -> None: + async def _wait_for_clearance(self, page: object, timeout_seconds: float | None = None) -> None: """检测并等待 Cloudflare 挑战完成 循环检测页面标题和内容: @@ -211,11 +226,21 @@ class CloudflareSessionManager: - 如果检测到挑战页面(如 "Just a moment"),继续等待 - 如果既非阻断也非挑战,认为已通过 + Args: + page: Playwright 页面对象 + timeout_seconds: 等待超时(秒);为 None 时使用新建会话的较长超时。 + 复用会话时传入较短超时,避免在无法通过的挑战上长时间空等。 + Raises: UpstreamBlockedError: 检测到被 Cloudflare 阻断 CloudflareChallengeError: 等待挑战超时 """ - timeout_at = asyncio.get_running_loop().time() + self._settings.cloudflare_wait_timeout_seconds + wait_timeout = ( + self._settings.cloudflare_wait_timeout_seconds + if timeout_seconds is None + else timeout_seconds + ) + timeout_at = asyncio.get_running_loop().time() + wait_timeout while True: title = (await page.title()).lower() content = (await page.content()).lower() @@ -238,7 +263,7 @@ class CloudflareSessionManager: logger.debug("自动点击 Turnstile 失败或未找到: %s", e) if asyncio.get_running_loop().time() >= timeout_at: - logger.warning("等待挑战超时(秒):%s", self._settings.cloudflare_wait_timeout_seconds) + logger.warning("等待挑战超时(秒):%s", wait_timeout) raise CloudflareChallengeError() await page.wait_for_timeout(1500)