cf逻辑复用
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user