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