cf逻辑复用

This commit is contained in:
2026-06-09 09:48:46 +08:00
parent a517e8c442
commit caa3e4342a
2 changed files with 68 additions and 30 deletions
+65
View File
@@ -76,6 +76,71 @@ class CloudflareSessionManager:
session = await self._create_verified_session(session_name=session_name, target_url=target_url)
return session
async def fetch_html(self, target_url: str) -> tuple[str, VerifiedSession]:
"""获取目标页面 HTML,并屏蔽会话复用失败的细节
统一处理三种情形,确保调用方拿到的要么是有效页面,要么是明确的上游异常:
1. 新建会话:直接复用创建时携带的页面 HTML,无需二次导航
2. 复用会话:带 storage_state 重新导航,并消化可能出现的 Cloudflare 挑战
3. 复用失败:主动失效后强制重建一次,对调用方透明(避免「一次成功一次失败」交替)
Args:
target_url: 目标页面 URL
Returns:
(html, session) 元组,session 为最终生效的会话
Raises:
UpstreamBlockedError: 会话重建后仍无法获取有效页面
CloudflareChallengeError: 等待 Cloudflare 挑战超时
"""
session = await self.get_verified_session(target_url)
if session.html is not None:
return session.html, session
html = await self._try_fetch_with_session(target_url, session)
if html is not None:
return html, session
# 复用会话已失效:失效并强制重建,新会话自带页面 HTML
logger.info("复用会话失败,强制重建会话:url=%s", target_url)
await self.invalidate(target_url)
session = await self.get_verified_session(target_url, force_refresh=True)
if session.html is not None:
return session.html, session
# 兜底:force_refresh 理论上必带 HTML,缺失时再导航一次
html = await self._try_fetch_with_session(target_url, session)
if html is None:
raise UpstreamBlockedError(f"会话重建后仍无法获取页面:url={target_url}")
return html, session
async def _try_fetch_with_session(self, target_url: str, session: VerifiedSession) -> str | None:
"""带已有会话导航并消化 Cloudflare 挑战
与新建路径保持一致:导航后等待挑战自动通过,而非一遇非 200 就判死。
Returns:
通过验证的页面 HTML;若判定会话已失效(非 200 或挑战/阻断),返回 None
"""
async with self._browser_pool.page_session(storage_state=session.storage_state) as (_, page):
response = await page.goto(
target_url,
wait_until="domcontentloaded",
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)
except (CloudflareChallengeError, UpstreamBlockedError):
logger.debug("复用会话仍处于挑战/阻断状态,判定会话失效:url=%s", target_url)
return None
return await page.content()
async def invalidate(self, target_url: str) -> None:
"""主动让会话失效