diff --git a/app/services/browser_pool.py b/app/services/browser_pool.py index bec0c74..02efd9d 100644 --- a/app/services/browser_pool.py +++ b/app/services/browser_pool.py @@ -25,6 +25,7 @@ class BrowserPool: def __init__(self, settings: Settings): self._settings = settings self._playwright: Any | None = None + self._playwright_cm: Any | None = None # stealth 钩子包装的 playwright 上下文管理器 self._browser: Any | None = None self._semaphore = asyncio.Semaphore(settings.max_site_concurrency) self._restart_lock = asyncio.Lock() @@ -66,15 +67,29 @@ class BrowserPool: headless = self._settings.browser_headless_effective logger.info( - "启动浏览器:enabled=%s headless=%s keep_open_on_error=%s channel=%s proxy=%s env=%s", + "启动浏览器:enabled=%s headless=%s keep_open_on_error=%s channel=%s proxy=%s stealth=%s env=%s", self._settings.browser_enabled, headless, self._settings.browser_keep_open_on_error_effective, self._settings.browser_channel, bool(self._settings.proxy_server), + self._settings.browser_stealth_enabled, self._settings.app_env, ) - self._playwright = await async_playwright().start() + if self._settings.browser_stealth_enabled: + # 用 playwright-stealth 钩子包装 playwright:后续 new_context/new_page + # 会自动应用 stealth(含 HTTP sec-ch-ua 客户端提示),并令 navigator.languages + # 与日语环境保持一致,避免指纹自相矛盾。 + from playwright_stealth import Stealth + + stealth = Stealth( + navigator_languages_override=("ja-JP", "ja", "en-US", "en"), + ) + self._playwright_cm = stealth.use_async(async_playwright()) + self._playwright = await self._playwright_cm.__aenter__() + else: + self._playwright_cm = None + self._playwright = await async_playwright().start() chromium = self._playwright.chromium launch_args = [ "--disable-blink-features=AutomationControlled", @@ -138,13 +153,20 @@ class BrowserPool: await self._browser.close() except Exception: logger.exception("关闭浏览器实例失败") - if self._playwright is not None: + if self._playwright_cm is not None: + # stealth 钩子包装:通过上下文管理器退出来停止 playwright + try: + await self._playwright_cm.__aexit__(None, None, None) + except Exception: + logger.exception("停止 Playwright(stealth 包装)运行时失败") + elif self._playwright is not None: try: await self._playwright.stop() except Exception: logger.exception("停止 Playwright 运行时失败") self._browser = None self._playwright = None + self._playwright_cm = None self._started = False async def _restart_browser(self, reason: str) -> None: @@ -251,9 +273,11 @@ class BrowserPool: logger.exception("关闭旧的保留调试上下文失败") async def _build_context_obj(self, storage_state: dict[str, Any] | None) -> Any: - """创建浏览器上下文(含断链重试与 stealth),仅负责创建本身。 + """创建浏览器上下文(含断链重试),仅负责创建本身。 计入回收阈值统计;不负责并发信号量与关闭,由调用方管理生命周期。 + 启用 stealth 时,playwright 已被 use_async 钩子包装,new_context 会自动 + 应用 stealth(含 HTTP sec-ch-ua),此处无需再手动 apply。 """ context = None for attempt in range(2): @@ -275,15 +299,6 @@ class BrowserPool: # 上下文创建成功,计入回收阈值统计 self._contexts_since_start += 1 - - if self._settings.browser_stealth_enabled: - try: - from playwright_stealth import Stealth - - await Stealth().apply_stealth_async(context) - except Exception as e: - logger.warning("应用 playwright-stealth 失败,隐身模式未生效, err=%s", e) - return context @staticmethod