cf逻辑复用

This commit is contained in:
2026-06-09 10:13:28 +08:00
parent caa3e4342a
commit ada64a68ad
2 changed files with 37 additions and 11 deletions
+1
View File
@@ -58,6 +58,7 @@ class Settings(BaseSettings):
page_acquire_timeout_seconds: float = 60.0 # 获取页面槽位超时 page_acquire_timeout_seconds: float = 60.0 # 获取页面槽位超时
request_timeout_seconds: float = 120.0 # 页面请求超时 request_timeout_seconds: float = 120.0 # 页面请求超时
cloudflare_wait_timeout_seconds: float = 60.0 # 等待 Cloudflare 挑战超时 cloudflare_wait_timeout_seconds: float = 60.0 # 等待 Cloudflare 挑战超时
cloudflare_reuse_wait_timeout_seconds: float = 8.0 # 复用会话时消化挑战的短超时,超时则放弃复用直接重建
session_ttl_seconds: int = 900 # 会话过期时间(秒) session_ttl_seconds: int = 900 # 会话过期时间(秒)
browser_locale: str = "ja-JP" browser_locale: str = "ja-JP"
browser_timezone_id: str = "Asia/Tokyo" browser_timezone_id: str = "Asia/Tokyo"
+36 -11
View File
@@ -119,9 +119,12 @@ class CloudflareSessionManager:
"""带已有会话导航并消化 Cloudflare 挑战 """带已有会话导航并消化 Cloudflare 挑战
与新建路径保持一致:导航后等待挑战自动通过,而非一遇非 200 就判死。 与新建路径保持一致:导航后等待挑战自动通过,而非一遇非 200 就判死。
CF 对携带过期 cf_clearance 的请求常直接返回挑战页(首响应可能非 200),
因此即便首响应非 200,也先给页面一次自动通过 Turnstile 的机会,
仅当挑战超时/被硬阻断、或通过后仍无有效内容时才判定会话失效。
Returns: Returns:
通过验证的页面 HTML;若判定会话已失效(非 200 或挑战/阻断),返回 None 通过验证的页面 HTML;若判定会话已失效,返回 None
""" """
async with self._browser_pool.page_session(storage_state=session.storage_state) as (_, page): async with self._browser_pool.page_session(storage_state=session.storage_state) as (_, page):
response = await page.goto( response = await page.goto(
@@ -130,16 +133,28 @@ class CloudflareSessionManager:
timeout=int(self._settings.request_timeout_seconds * 1000), timeout=int(self._settings.request_timeout_seconds * 1000),
) )
status = response.status if response is not None else None 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: 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): 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 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: async def invalidate(self, target_url: str) -> None:
"""主动让会话失效 """主动让会话失效
@@ -203,7 +218,7 @@ class CloudflareSessionManager:
html=html_content, 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 挑战完成 """检测并等待 Cloudflare 挑战完成
循环检测页面标题和内容: 循环检测页面标题和内容:
@@ -211,11 +226,21 @@ class CloudflareSessionManager:
- 如果检测到挑战页面(如 "Just a moment"),继续等待 - 如果检测到挑战页面(如 "Just a moment"),继续等待
- 如果既非阻断也非挑战,认为已通过 - 如果既非阻断也非挑战,认为已通过
Args:
page: Playwright 页面对象
timeout_seconds: 等待超时(秒);为 None 时使用新建会话的较长超时。
复用会话时传入较短超时,避免在无法通过的挑战上长时间空等。
Raises: Raises:
UpstreamBlockedError: 检测到被 Cloudflare 阻断 UpstreamBlockedError: 检测到被 Cloudflare 阻断
CloudflareChallengeError: 等待挑战超时 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: while True:
title = (await page.title()).lower() title = (await page.title()).lower()
content = (await page.content()).lower() content = (await page.content()).lower()
@@ -238,7 +263,7 @@ class CloudflareSessionManager:
logger.debug("自动点击 Turnstile 失败或未找到: %s", e) logger.debug("自动点击 Turnstile 失败或未找到: %s", e)
if asyncio.get_running_loop().time() >= timeout_at: if asyncio.get_running_loop().time() >= timeout_at:
logger.warning("等待挑战超时(秒):%s", self._settings.cloudflare_wait_timeout_seconds) logger.warning("等待挑战超时(秒):%s", wait_timeout)
raise CloudflareChallengeError() raise CloudflareChallengeError()
await page.wait_for_timeout(1500) await page.wait_for_timeout(1500)