2026-01-06 19:36:42 +08:00
|
|
|
|
"""
|
|
|
|
|
|
浏览器池管理模块
|
|
|
|
|
|
管理Playwright浏览器实例的生命周期,支持复用以提升性能
|
|
|
|
|
|
"""
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
import time
|
|
|
|
|
|
from typing import Optional, Dict, Any
|
|
|
|
|
|
from playwright.async_api import async_playwright, Browser, BrowserContext, Page
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BrowserPool:
|
|
|
|
|
|
"""浏览器池管理器(单例模式)"""
|
|
|
|
|
|
|
2026-01-07 22:55:12 +08:00
|
|
|
|
def __init__(self, idle_timeout: int = 1800, max_instances: int = 20, headless: bool = True):
|
2026-01-06 19:36:42 +08:00
|
|
|
|
"""
|
|
|
|
|
|
初始化浏览器池
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
idle_timeout: 空闲超时时间(秒),默认30分钟(已禁用,保持常驻)
|
2026-01-07 22:55:12 +08:00
|
|
|
|
max_instances: 最大浏览器实例数,默认20个(支持更多并发)
|
2026-01-06 19:36:42 +08:00
|
|
|
|
headless: 是否使用无头模式,False为有头模式(方便调试)
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.playwright = None
|
|
|
|
|
|
self.browser: Optional[Browser] = None
|
|
|
|
|
|
self.context: Optional[BrowserContext] = None
|
|
|
|
|
|
self.page: Optional[Page] = None
|
|
|
|
|
|
self.last_used_time = 0
|
|
|
|
|
|
self.idle_timeout = idle_timeout
|
|
|
|
|
|
self.max_instances = max_instances
|
|
|
|
|
|
self.headless = headless
|
|
|
|
|
|
self.is_initializing = False
|
|
|
|
|
|
self.init_lock = asyncio.Lock()
|
|
|
|
|
|
self.is_preheated = False # 标记是否已预热
|
|
|
|
|
|
|
|
|
|
|
|
# 临时浏览器实例池(用于并发请求)
|
|
|
|
|
|
self.temp_browsers: Dict[str, Dict] = {} # {session_id: {browser, context, page, created_at}}
|
|
|
|
|
|
self.temp_lock = asyncio.Lock()
|
|
|
|
|
|
|
2026-01-07 22:55:12 +08:00
|
|
|
|
# 请求队列:当超过max_instances时排队等待
|
|
|
|
|
|
self.waiting_queue: asyncio.Queue = asyncio.Queue()
|
|
|
|
|
|
self.queue_processing = False
|
|
|
|
|
|
|
|
|
|
|
|
# 扫码登录专用:页面隔离池(共享浏览器和context,但每个用户独立page)
|
|
|
|
|
|
self.qrcode_pages: Dict[str, Dict] = {} # {session_id: {page, created_at}}
|
|
|
|
|
|
self.qrcode_lock = asyncio.Lock()
|
|
|
|
|
|
|
2026-01-06 19:36:42 +08:00
|
|
|
|
print(f"[浏览器池] 已创建,常驻模式(不自动清理),最大实例数: {max_instances}", file=sys.stderr)
|
|
|
|
|
|
|
2026-01-07 22:55:12 +08:00
|
|
|
|
async def get_browser(self, cookies: Optional[list] = None, proxy: Optional[dict] = None,
|
2026-01-06 19:36:42 +08:00
|
|
|
|
user_agent: Optional[str] = None, session_id: Optional[str] = None,
|
2026-01-07 22:55:12 +08:00
|
|
|
|
headless: Optional[bool] = None, force_new: bool = False) -> tuple[Browser, BrowserContext, Page]:
|
2026-01-06 19:36:42 +08:00
|
|
|
|
"""
|
|
|
|
|
|
获取浏览器实例(复用或新建)
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
cookies: 可选的Cookie列表
|
2026-01-07 22:55:12 +08:00
|
|
|
|
proxy: 可选的代理配置,格式: {"server": "...", "username": "...", "password": "..."}
|
2026-01-06 19:36:42 +08:00
|
|
|
|
user_agent: 可选的自定义User-Agent
|
|
|
|
|
|
session_id: 会话 ID,用于区分不同的并发请求
|
|
|
|
|
|
headless: 可选的headless模式,为None时使用默认配置
|
2026-01-07 22:55:12 +08:00
|
|
|
|
force_new: 是否强制创建全新浏览器(即使session_id已存在)
|
2026-01-06 19:36:42 +08:00
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
(browser, context, page) 三元组
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 如果没有指定headless,使用默认配置
|
|
|
|
|
|
if headless is None:
|
|
|
|
|
|
headless = self.headless
|
|
|
|
|
|
# 如果主浏览器可用且无会话 ID,使用主浏览器
|
|
|
|
|
|
if not session_id:
|
|
|
|
|
|
async with self.init_lock:
|
|
|
|
|
|
# 检查现有浏览器是否可用
|
|
|
|
|
|
if await self._is_browser_alive():
|
|
|
|
|
|
print("[浏览器池] 复用主浏览器实例", file=sys.stderr)
|
|
|
|
|
|
self.last_used_time = time.time()
|
|
|
|
|
|
|
|
|
|
|
|
# 如果需要注入Cookie,直接添加到现有的context(不创建新context)
|
|
|
|
|
|
if cookies:
|
|
|
|
|
|
print(f"[浏览器池] 在现有context中注入 {len(cookies)} 个Cookie", file=sys.stderr)
|
|
|
|
|
|
await self.context.add_cookies(cookies)
|
|
|
|
|
|
|
|
|
|
|
|
return self.browser, self.context, self.page
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 创建新浏览器
|
|
|
|
|
|
print("[浏览器池] 创建主浏览器实例", file=sys.stderr)
|
|
|
|
|
|
await self._init_browser(cookies, proxy, user_agent)
|
|
|
|
|
|
self.last_used_time = time.time()
|
|
|
|
|
|
return self.browser, self.context, self.page
|
|
|
|
|
|
|
|
|
|
|
|
# 并发请求:复用或创建临时浏览器
|
|
|
|
|
|
else:
|
|
|
|
|
|
async with self.temp_lock:
|
|
|
|
|
|
# 首先检查是否已存在该session_id的临时浏览器
|
2026-01-07 22:55:12 +08:00
|
|
|
|
if session_id in self.temp_browsers and not force_new:
|
2026-01-06 19:36:42 +08:00
|
|
|
|
print(f"[浏览器池] 复用会话 {session_id} 的临时浏览器", file=sys.stderr)
|
|
|
|
|
|
browser_info = self.temp_browsers[session_id]
|
|
|
|
|
|
return browser_info["browser"], browser_info["context"], browser_info["page"]
|
|
|
|
|
|
|
2026-01-07 22:55:12 +08:00
|
|
|
|
# 强制创建全新浏览器:先释放旧的
|
|
|
|
|
|
if force_new and session_id in self.temp_browsers:
|
|
|
|
|
|
print(f"[浏览器池] force_new=True,释放旧的会话 {session_id}", file=sys.stderr)
|
|
|
|
|
|
old_browser_info = self.temp_browsers[session_id]
|
|
|
|
|
|
try:
|
|
|
|
|
|
await old_browser_info["page"].close()
|
|
|
|
|
|
await old_browser_info["context"].close()
|
|
|
|
|
|
await old_browser_info["browser"].close()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[浏览器池] 释放旧浏览器失败: {str(e)}", file=sys.stderr)
|
|
|
|
|
|
finally:
|
|
|
|
|
|
del self.temp_browsers[session_id]
|
|
|
|
|
|
|
2026-01-06 19:36:42 +08:00
|
|
|
|
# 检查是否超过最大实例数
|
|
|
|
|
|
if len(self.temp_browsers) >= self.max_instances - 1: # -1 留给主浏览器
|
|
|
|
|
|
print(f"[浏览器池] ⚠️ 已达最大实例数 ({self.max_instances}),等待释放...", file=sys.stderr)
|
2026-01-07 22:55:12 +08:00
|
|
|
|
|
|
|
|
|
|
# 等待最多30秒,每秒1秒检查一次
|
|
|
|
|
|
for i in range(30):
|
|
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
|
|
if len(self.temp_browsers) < self.max_instances - 1:
|
|
|
|
|
|
print(f"[浏览器池] 检测到空闲实例,继续创建", file=sys.stderr)
|
|
|
|
|
|
break
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 超时30秒仍满,返回错误
|
|
|
|
|
|
raise Exception(f"浏览器实例数已满,请稍后再试")
|
2026-01-06 19:36:42 +08:00
|
|
|
|
|
|
|
|
|
|
print(f"[浏览器池] 为会话 {session_id} 创建临时浏览器 ({len(self.temp_browsers)+1}/{self.max_instances-1})", file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
|
|
# 创建临时浏览器,传入headless参数
|
|
|
|
|
|
browser, context, page = await self._create_temp_browser(cookies, proxy, user_agent, headless)
|
|
|
|
|
|
|
|
|
|
|
|
# 保存到临时池
|
|
|
|
|
|
self.temp_browsers[session_id] = {
|
|
|
|
|
|
"browser": browser,
|
|
|
|
|
|
"context": context,
|
|
|
|
|
|
"page": page,
|
|
|
|
|
|
"created_at": time.time()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return browser, context, page
|
|
|
|
|
|
|
|
|
|
|
|
async def _is_browser_alive(self) -> bool:
|
|
|
|
|
|
"""检查浏览器是否存活(不检查超时,保持常驻)"""
|
|
|
|
|
|
if not self.browser or not self.context or not self.page:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
# 注意:为了保持浏览器常驻,不再检查空闲超时
|
|
|
|
|
|
# 原代码:
|
|
|
|
|
|
# if time.time() - self.last_used_time > self.idle_timeout:
|
|
|
|
|
|
# print(f"[浏览器池] 浏览器空闲超时 ({self.idle_timeout}秒),需要重建", file=sys.stderr)
|
|
|
|
|
|
# await self.close()
|
|
|
|
|
|
# return False
|
|
|
|
|
|
|
|
|
|
|
|
# 检查浏览器是否仍在运行
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 尝试获取页面标题来验证连接
|
|
|
|
|
|
await self.page.title()
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[浏览器池] 浏览器连接失效: {str(e)}", file=sys.stderr)
|
|
|
|
|
|
await self.close()
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
2026-01-07 22:55:12 +08:00
|
|
|
|
async def _init_browser(self, cookies: Optional[list] = None, proxy: Optional[dict] = None,
|
2026-01-06 19:36:42 +08:00
|
|
|
|
user_agent: Optional[str] = None):
|
2026-01-07 22:55:12 +08:00
|
|
|
|
"""初始化新浏览器实例。proxy为dict格式: {"server": "...", "username": "...", "password": "..."}"""
|
2026-01-06 19:36:42 +08:00
|
|
|
|
try:
|
|
|
|
|
|
# 启动Playwright
|
|
|
|
|
|
if not self.playwright:
|
|
|
|
|
|
# Windows环境下,需要设置事件循环策略
|
|
|
|
|
|
if sys.platform == 'win32':
|
|
|
|
|
|
# 设置为ProactorEventLoop或SelectorEventLoop
|
|
|
|
|
|
try:
|
|
|
|
|
|
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[浏览器池] 警告: 设置事件循环策略失败: {str(e)}", file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
|
|
self.playwright = await async_playwright().start()
|
|
|
|
|
|
print("[浏览器池] Playwright启动成功", file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
|
|
# 启动浏览器(性能优先配置)
|
|
|
|
|
|
launch_kwargs = {
|
|
|
|
|
|
"headless": self.headless, # 使用配置的headless参数
|
|
|
|
|
|
"args": [
|
|
|
|
|
|
'--disable-blink-features=AutomationControlled', # 隐藏自动化特征
|
|
|
|
|
|
'--no-sandbox', # Linux环境必需
|
|
|
|
|
|
'--disable-setuid-sandbox',
|
|
|
|
|
|
'--disable-dev-shm-usage', # 使用/tmp而非/dev/shm,避免内存不足
|
|
|
|
|
|
|
|
|
|
|
|
# 性能优化
|
|
|
|
|
|
'--disable-web-security', # 禁用同源策略(提升加载速度)
|
|
|
|
|
|
'--disable-features=IsolateOrigins,site-per-process', # 禁用站点隔离(提升性能)
|
|
|
|
|
|
'--disable-site-isolation-trials',
|
|
|
|
|
|
'--enable-features=NetworkService,NetworkServiceInProcess', # 网络服务优化
|
|
|
|
|
|
'--disable-background-timer-throttling', # 禁用后台限速
|
|
|
|
|
|
'--disable-backgrounding-occluded-windows',
|
|
|
|
|
|
'--disable-renderer-backgrounding', # 渲染进程不降优先级
|
|
|
|
|
|
'--disable-background-networking',
|
|
|
|
|
|
|
|
|
|
|
|
# 缓存和存储优化
|
|
|
|
|
|
'--disk-cache-size=268435456', # 256MB磁盘缓存
|
|
|
|
|
|
'--media-cache-size=134217728', # 128MB媒体缓存
|
|
|
|
|
|
|
|
|
|
|
|
# 渲染优化(保留GPU支持)
|
|
|
|
|
|
'--enable-gpu-rasterization', # 启用GPU光栅化
|
|
|
|
|
|
'--enable-zero-copy', # 零拷贝优化
|
|
|
|
|
|
'--ignore-gpu-blocklist', # 忽略GPU黑名单
|
|
|
|
|
|
'--enable-accelerated-2d-canvas', # 加速2D canvas
|
|
|
|
|
|
|
|
|
|
|
|
# 网络优化
|
|
|
|
|
|
'--enable-quic', # 启用QUIC协议
|
|
|
|
|
|
'--enable-tcp-fast-open', # TCP快速打开
|
|
|
|
|
|
'--max-connections-per-host=10', # 每个主机最大连接数
|
|
|
|
|
|
|
|
|
|
|
|
# 减少不必要的功能
|
|
|
|
|
|
'--disable-extensions',
|
|
|
|
|
|
'--disable-breakpad', # 禁用崩溃报告
|
|
|
|
|
|
'--disable-component-extensions-with-background-pages',
|
|
|
|
|
|
'--disable-ipc-flooding-protection', # 禁用IPC洪水保护(提升性能)
|
|
|
|
|
|
'--disable-hang-monitor', # 禁用挂起监控
|
|
|
|
|
|
'--disable-prompt-on-repost',
|
|
|
|
|
|
'--disable-domain-reliability',
|
|
|
|
|
|
'--disable-component-update',
|
|
|
|
|
|
|
|
|
|
|
|
# 界面优化
|
|
|
|
|
|
'--hide-scrollbars',
|
|
|
|
|
|
'--mute-audio',
|
|
|
|
|
|
'--no-first-run',
|
|
|
|
|
|
'--no-default-browser-check',
|
|
|
|
|
|
'--metrics-recording-only',
|
|
|
|
|
|
'--force-color-profile=srgb',
|
|
|
|
|
|
],
|
|
|
|
|
|
}
|
|
|
|
|
|
if proxy:
|
2026-01-07 22:55:12 +08:00
|
|
|
|
launch_kwargs["proxy"] = proxy # proxy已经是dict格式,直接使用
|
2026-01-06 19:36:42 +08:00
|
|
|
|
|
|
|
|
|
|
self.browser = await self.playwright.chromium.launch(**launch_kwargs)
|
|
|
|
|
|
print("[浏览器池] Chromium浏览器启动成功", file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
|
|
# 创建上下文
|
|
|
|
|
|
await self._create_new_context(cookies, proxy, user_agent)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[浏览器池] 初始化浏览器失败: {str(e)}", file=sys.stderr)
|
|
|
|
|
|
await self.close()
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
2026-01-07 22:55:12 +08:00
|
|
|
|
async def _create_new_context(self, cookies: Optional[list] = None, proxy: Optional[dict] = None,
|
2026-01-06 19:36:42 +08:00
|
|
|
|
user_agent: Optional[str] = None):
|
2026-01-07 22:55:12 +08:00
|
|
|
|
"""创建新的浏览器上下文。proxy为dict格式: {"server": "...", "username": "...", "password": "..."}"""
|
2026-01-06 19:36:42 +08:00
|
|
|
|
try:
|
|
|
|
|
|
# 关闭旧上下文
|
|
|
|
|
|
if self.context:
|
|
|
|
|
|
await self.context.close()
|
|
|
|
|
|
print("[浏览器池] 已关闭旧上下文", file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
|
|
# 创建新上下文
|
|
|
|
|
|
context_kwargs = {
|
|
|
|
|
|
"viewport": {'width': 1280, 'height': 720},
|
|
|
|
|
|
"user_agent": user_agent or 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
|
|
|
|
}
|
|
|
|
|
|
self.context = await self.browser.new_context(**context_kwargs)
|
|
|
|
|
|
|
2026-01-07 22:55:12 +08:00
|
|
|
|
# 注入反检测脚本(关键)
|
|
|
|
|
|
await self.context.add_init_script("""
|
|
|
|
|
|
// 移除webdriver标记
|
|
|
|
|
|
Object.defineProperty(navigator, 'webdriver', {
|
|
|
|
|
|
get: () => undefined
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 隐藏chrome自动化特征
|
|
|
|
|
|
window.chrome = {
|
|
|
|
|
|
runtime: {}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 模拟plugins
|
|
|
|
|
|
Object.defineProperty(navigator, 'plugins', {
|
|
|
|
|
|
get: () => [
|
|
|
|
|
|
{
|
|
|
|
|
|
0: {type: "application/x-google-chrome-pdf", suffixes: "pdf", description: "Portable Document Format"},
|
|
|
|
|
|
description: "Portable Document Format",
|
|
|
|
|
|
filename: "internal-pdf-viewer",
|
|
|
|
|
|
length: 1,
|
|
|
|
|
|
name: "Chrome PDF Plugin"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
0: {type: "application/pdf", suffixes: "pdf", description: ""},
|
|
|
|
|
|
description: "",
|
|
|
|
|
|
filename: "mhjfbmdgcfjbbpaeojofohoefgiehjai",
|
|
|
|
|
|
length: 1,
|
|
|
|
|
|
name: "Chrome PDF Viewer"
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 模拟permissions API
|
|
|
|
|
|
const originalQuery = window.navigator.permissions.query;
|
|
|
|
|
|
window.navigator.permissions.query = (parameters) => (
|
|
|
|
|
|
parameters.name === 'notifications' ?
|
|
|
|
|
|
Promise.resolve({ state: Notification.permission }) :
|
|
|
|
|
|
originalQuery(parameters)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 阻止检测自动化的网络请求
|
|
|
|
|
|
const originalFetch = window.fetch;
|
|
|
|
|
|
window.fetch = function(...args) {
|
|
|
|
|
|
const url = args[0];
|
|
|
|
|
|
if (typeof url === 'string' && (
|
|
|
|
|
|
url.includes('127.0.0.1:9222') ||
|
|
|
|
|
|
url.includes('localhost:9222') ||
|
|
|
|
|
|
url.includes('chrome-extension://invalid')
|
|
|
|
|
|
)) {
|
|
|
|
|
|
return Promise.reject(new Error('blocked'));
|
|
|
|
|
|
}
|
|
|
|
|
|
return originalFetch.apply(this, args);
|
|
|
|
|
|
};
|
|
|
|
|
|
""")
|
|
|
|
|
|
print("[浏览器池] 已注入反检测脚本", file=sys.stderr)
|
|
|
|
|
|
|
2026-01-06 19:36:42 +08:00
|
|
|
|
# 注入Cookie
|
|
|
|
|
|
if cookies:
|
|
|
|
|
|
await self.context.add_cookies(cookies)
|
|
|
|
|
|
print(f"[浏览器池] 已注入 {len(cookies)} 个Cookie", file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
|
|
# 创建页面
|
|
|
|
|
|
self.page = await self.context.new_page()
|
|
|
|
|
|
print("[浏览器池] 新页面创建成功", file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[浏览器池] 创建上下文失败: {str(e)}", file=sys.stderr)
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
async def close(self):
|
|
|
|
|
|
"""关闭浏览器池"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if self.page:
|
|
|
|
|
|
await self.page.close()
|
|
|
|
|
|
self.page = None
|
|
|
|
|
|
if self.context:
|
|
|
|
|
|
await self.context.close()
|
|
|
|
|
|
self.context = None
|
|
|
|
|
|
if self.browser:
|
|
|
|
|
|
await self.browser.close()
|
|
|
|
|
|
self.browser = None
|
|
|
|
|
|
if self.playwright:
|
|
|
|
|
|
await self.playwright.stop()
|
|
|
|
|
|
self.playwright = None
|
|
|
|
|
|
print("[浏览器池] 浏览器已关闭", file=sys.stderr)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[浏览器池] 关闭浏览器异常: {str(e)}", file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
|
|
async def cleanup_if_idle(self):
|
|
|
|
|
|
"""清理空闲浏览器(定时任务调用)- 已禁用,保持常驻"""
|
|
|
|
|
|
# 注意:为了保持浏览器常驻,不再自动清理
|
|
|
|
|
|
# 原代码:
|
|
|
|
|
|
# if self.browser and time.time() - self.last_used_time > self.idle_timeout:
|
|
|
|
|
|
# print(f"[浏览器池] 检测到空闲超时,自动清理浏览器", file=sys.stderr)
|
|
|
|
|
|
# await self.close()
|
|
|
|
|
|
pass # 不再执行清理操作
|
|
|
|
|
|
|
|
|
|
|
|
async def preheat(self, target_url: str = "https://creator.xiaohongshu.com/login"):
|
|
|
|
|
|
"""
|
|
|
|
|
|
预热浏览器:提前初始化并访问目标页面
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
target_url: 预热目标页面,默认为小红书登录页
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
print("[浏览器预热] 开始预热浏览器...", file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
|
|
# 初始化浏览器
|
|
|
|
|
|
await self._init_browser()
|
|
|
|
|
|
self.last_used_time = time.time()
|
|
|
|
|
|
|
|
|
|
|
|
# 访问目标页面
|
|
|
|
|
|
print(f"[浏览器预热] 正在访问: {target_url}", file=sys.stderr)
|
|
|
|
|
|
await self.page.goto(target_url, wait_until='domcontentloaded', timeout=45000)
|
|
|
|
|
|
|
|
|
|
|
|
# 等待页面完全加载
|
|
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
|
|
|
|
|
|
|
|
self.is_preheated = True
|
|
|
|
|
|
print("[浏览器预热] ✅ 预热完成,浏览器已就绪!", file=sys.stderr)
|
|
|
|
|
|
print(f"[浏览器预热] 当前页面: {self.page.url}", file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[浏览器预热] ⚠️ 预热失败: {str(e)}", file=sys.stderr)
|
|
|
|
|
|
print("[浏览器预热] 将在首次使用时再初始化", file=sys.stderr)
|
|
|
|
|
|
self.is_preheated = False
|
|
|
|
|
|
|
|
|
|
|
|
async def repreheat(self, target_url: str = "https://creator.xiaohongshu.com/login"):
|
|
|
|
|
|
"""
|
|
|
|
|
|
补充预热:在后台重新将浏览器预热到目标页面
|
|
|
|
|
|
用于在主浏览器被使用后,重新预热以保证下次使用的性能
|
|
|
|
|
|
|
|
|
|
|
|
重要:如果浏览器正在使用中(有临时实例),跳过预热避免干扰
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
target_url: 预热目标页面,默认为小红书登录页
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 关键优化:检查是否有临时浏览器正在使用
|
|
|
|
|
|
if len(self.temp_browsers) > 0:
|
|
|
|
|
|
print(f"[浏览器补充预热] 检测到 {len(self.temp_browsers)} 个临时浏览器正在使用,跳过预热避免干扰", file=sys.stderr)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 检查主浏览器是否正在被使用(通过最近使用时间判断)
|
|
|
|
|
|
time_since_last_use = time.time() - self.last_used_time
|
|
|
|
|
|
if time_since_last_use < 10: # 最近10秒内使用过,可能还在操作中
|
|
|
|
|
|
print(f"[浏览器补充预热] 主浏览器最近 {time_since_last_use:.1f}秒前被使用,可能还在操作中,跳过预热", file=sys.stderr)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
max_retries = 3
|
|
|
|
|
|
retry_count = 0
|
|
|
|
|
|
|
|
|
|
|
|
while retry_count < max_retries:
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 检查主浏览器是否存活
|
|
|
|
|
|
if not await self._is_browser_alive():
|
|
|
|
|
|
print(f"[浏览器补充预热] 浏览器未初始化,执行完整预热 (尝试 {retry_count + 1}/{max_retries})", file=sys.stderr)
|
|
|
|
|
|
await self.preheat(target_url)
|
|
|
|
|
|
self.is_preheated = True
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否已经在目标页面
|
|
|
|
|
|
current_url = self.page.url if self.page else ""
|
|
|
|
|
|
if target_url in current_url:
|
|
|
|
|
|
print(f"[浏览器补充预热] 已在目标页面,无需补充预热: {current_url}", file=sys.stderr)
|
|
|
|
|
|
self.is_preheated = True
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
print(f"[浏览器补充预热] 开始补充预热... (尝试 {retry_count + 1}/{max_retries})", file=sys.stderr)
|
|
|
|
|
|
print(f"[浏览器补充预热] 当前页面: {current_url}", file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
|
|
# 再次检查是否有新的临时浏览器(双重检查)
|
|
|
|
|
|
if len(self.temp_browsers) > 0:
|
|
|
|
|
|
print(f"[浏览器补充预热] 检测到新的临时浏览器启动,取消预热", file=sys.stderr)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 访问目标页面
|
|
|
|
|
|
print(f"[浏览器补充预热] 正在访问: {target_url}", file=sys.stderr)
|
|
|
|
|
|
await self.page.goto(target_url, wait_until='domcontentloaded', timeout=45000)
|
|
|
|
|
|
|
|
|
|
|
|
# 额外等待,确保页面完全加载
|
|
|
|
|
|
await asyncio.sleep(2)
|
|
|
|
|
|
|
|
|
|
|
|
# 验证页面是否正确加载
|
|
|
|
|
|
current_page_url = self.page.url
|
|
|
|
|
|
if target_url in current_page_url or 'creator.xiaohongshu.com' in current_page_url:
|
|
|
|
|
|
self.is_preheated = True
|
|
|
|
|
|
self.last_used_time = time.time()
|
|
|
|
|
|
print("[浏览器补充预热] ✅ 补充预热完成!", file=sys.stderr)
|
|
|
|
|
|
print(f"[浏览器补充预热] 当前页面: {current_page_url}", file=sys.stderr)
|
|
|
|
|
|
return # 成功,退出重试循环
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"[浏览器补充预热] 页面未正确加载,期望: {target_url}, 实际: {current_page_url}", file=sys.stderr)
|
|
|
|
|
|
raise Exception(f"页面未正确加载到目标地址")
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
retry_count += 1
|
|
|
|
|
|
print(f"[浏览器补充预热] ⚠️ 补充预热失败 (尝试 {retry_count}/{max_retries}): {str(e)}", file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
|
|
if retry_count < max_retries:
|
|
|
|
|
|
# 等待一段时间后重试
|
|
|
|
|
|
await asyncio.sleep(2)
|
|
|
|
|
|
# 尝试重新初始化浏览器
|
|
|
|
|
|
try:
|
|
|
|
|
|
await self.close() # 关闭当前可能有问题的浏览器
|
|
|
|
|
|
except:
|
|
|
|
|
|
pass # 忽略关闭时的错误
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 所有重试都失败了
|
|
|
|
|
|
print(f"[浏览器补充预热] ❌ 所有重试都失败了,将尝试完整预热", file=sys.stderr)
|
|
|
|
|
|
try:
|
|
|
|
|
|
await self.close() # 先关闭当前浏览器
|
|
|
|
|
|
except:
|
|
|
|
|
|
pass
|
|
|
|
|
|
# 执行完整预热
|
|
|
|
|
|
try:
|
|
|
|
|
|
await self.preheat(target_url)
|
|
|
|
|
|
self.is_preheated = True
|
|
|
|
|
|
return
|
|
|
|
|
|
except Exception as final_error:
|
|
|
|
|
|
print(f"[浏览器补充预热] ❌ 最终预热也失败: {str(final_error)}", file=sys.stderr)
|
|
|
|
|
|
self.is_preheated = False
|
|
|
|
|
|
# 即使最终失败,也要确保浏览器处于可用状态
|
|
|
|
|
|
try:
|
|
|
|
|
|
await self._init_browser()
|
|
|
|
|
|
except:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
2026-01-07 22:55:12 +08:00
|
|
|
|
async def _create_temp_browser(self, cookies: Optional[list] = None, proxy: Optional[dict] = None,
|
2026-01-06 19:36:42 +08:00
|
|
|
|
user_agent: Optional[str] = None, headless: bool = True) -> tuple[Browser, BrowserContext, Page]:
|
|
|
|
|
|
"""创建临时浏览器实例(用于并发请求)
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
cookies: Cookie列表
|
2026-01-07 22:55:12 +08:00
|
|
|
|
proxy: 代理配置,格式: {"server": "...", "username": "...", "password": "..."}
|
2026-01-06 19:36:42 +08:00
|
|
|
|
user_agent: 自定义User-Agent
|
|
|
|
|
|
headless: 是否使用无头模式
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 启动Playwright(复用全局实例)
|
|
|
|
|
|
if not self.playwright:
|
|
|
|
|
|
if sys.platform == 'win32':
|
|
|
|
|
|
try:
|
|
|
|
|
|
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[临时浏览器] 警告: 设置事件循环策略失败: {str(e)}", file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
|
|
self.playwright = await async_playwright().start()
|
|
|
|
|
|
|
|
|
|
|
|
# 启动浏览器(临时实例,性能优先配置)
|
|
|
|
|
|
launch_kwargs = {
|
2026-01-07 22:55:12 +08:00
|
|
|
|
"headless": headless,
|
2026-01-06 19:36:42 +08:00
|
|
|
|
"args": [
|
|
|
|
|
|
'--disable-blink-features=AutomationControlled',
|
|
|
|
|
|
'--no-sandbox',
|
|
|
|
|
|
'--disable-setuid-sandbox',
|
|
|
|
|
|
'--disable-dev-shm-usage',
|
|
|
|
|
|
|
2026-01-07 22:55:12 +08:00
|
|
|
|
# 性能优化 - 减少资源占用
|
2026-01-06 19:36:42 +08:00
|
|
|
|
'--disable-web-security',
|
|
|
|
|
|
'--disable-features=IsolateOrigins,site-per-process',
|
|
|
|
|
|
'--disable-site-isolation-trials',
|
|
|
|
|
|
'--enable-features=NetworkService,NetworkServiceInProcess',
|
|
|
|
|
|
'--disable-background-timer-throttling',
|
|
|
|
|
|
'--disable-backgrounding-occluded-windows',
|
|
|
|
|
|
'--disable-renderer-backgrounding',
|
|
|
|
|
|
'--disable-background-networking',
|
|
|
|
|
|
|
2026-01-07 22:55:12 +08:00
|
|
|
|
# 缓存优化 - 减小缓存以节省内存
|
|
|
|
|
|
'--disk-cache-size=67108864', # 64MB(原256MB)
|
|
|
|
|
|
'--media-cache-size=33554432', # 32MB(原128MB)
|
2026-01-06 19:36:42 +08:00
|
|
|
|
|
2026-01-07 22:55:12 +08:00
|
|
|
|
# 渲染优化 - 禁用GPU以减少资源占用
|
|
|
|
|
|
'--disable-gpu',
|
|
|
|
|
|
'--disable-accelerated-2d-canvas',
|
|
|
|
|
|
'--disable-accelerated-video-decode',
|
2026-01-06 19:36:42 +08:00
|
|
|
|
|
|
|
|
|
|
# 网络优化
|
|
|
|
|
|
'--enable-quic',
|
|
|
|
|
|
'--enable-tcp-fast-open',
|
2026-01-07 22:55:12 +08:00
|
|
|
|
'--max-connections-per-host=6', # 减少连接数(原10)
|
2026-01-06 19:36:42 +08:00
|
|
|
|
|
|
|
|
|
|
# 减少不必要的功能
|
|
|
|
|
|
'--disable-extensions',
|
|
|
|
|
|
'--disable-breakpad',
|
|
|
|
|
|
'--disable-component-extensions-with-background-pages',
|
|
|
|
|
|
'--disable-ipc-flooding-protection',
|
|
|
|
|
|
'--disable-hang-monitor',
|
|
|
|
|
|
'--disable-prompt-on-repost',
|
|
|
|
|
|
'--disable-domain-reliability',
|
|
|
|
|
|
'--disable-component-update',
|
2026-01-07 22:55:12 +08:00
|
|
|
|
'--disable-plugins',
|
|
|
|
|
|
'--disable-sync',
|
|
|
|
|
|
'--disable-translate',
|
2026-01-06 19:36:42 +08:00
|
|
|
|
|
|
|
|
|
|
# 界面优化
|
|
|
|
|
|
'--hide-scrollbars',
|
|
|
|
|
|
'--mute-audio',
|
|
|
|
|
|
'--no-first-run',
|
|
|
|
|
|
'--no-default-browser-check',
|
|
|
|
|
|
'--metrics-recording-only',
|
2026-01-07 22:55:12 +08:00
|
|
|
|
|
|
|
|
|
|
# 内存优化
|
|
|
|
|
|
'--js-flags=--max-old-space-size=512', # 限制JS堆内存
|
2026-01-06 19:36:42 +08:00
|
|
|
|
],
|
|
|
|
|
|
}
|
|
|
|
|
|
if proxy:
|
2026-01-07 22:55:12 +08:00
|
|
|
|
launch_kwargs["proxy"] = proxy # proxy已经是dict格式,直接使用
|
2026-01-06 19:36:42 +08:00
|
|
|
|
|
|
|
|
|
|
browser = await self.playwright.chromium.launch(**launch_kwargs)
|
|
|
|
|
|
|
2026-01-07 22:55:12 +08:00
|
|
|
|
# 创建上下文(使用隐身模式,确保无痕迹)
|
2026-01-06 19:36:42 +08:00
|
|
|
|
context_kwargs = {
|
|
|
|
|
|
"viewport": {'width': 1280, 'height': 720},
|
|
|
|
|
|
"user_agent": user_agent or 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
2026-01-07 22:55:12 +08:00
|
|
|
|
"no_viewport": False,
|
|
|
|
|
|
"ignore_https_errors": True,
|
|
|
|
|
|
# 不使用storage_state,确保完全干净
|
2026-01-06 19:36:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
context = await browser.new_context(**context_kwargs)
|
|
|
|
|
|
|
2026-01-07 22:55:12 +08:00
|
|
|
|
# 注入反检测脚本(关键)
|
|
|
|
|
|
await context.add_init_script("""
|
|
|
|
|
|
// 移除webdriver标记
|
|
|
|
|
|
Object.defineProperty(navigator, 'webdriver', {
|
|
|
|
|
|
get: () => undefined
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 隐藏chrome自动化特征
|
|
|
|
|
|
window.chrome = {
|
|
|
|
|
|
runtime: {}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 模拟plugins
|
|
|
|
|
|
Object.defineProperty(navigator, 'plugins', {
|
|
|
|
|
|
get: () => [
|
|
|
|
|
|
{
|
|
|
|
|
|
0: {type: "application/x-google-chrome-pdf", suffixes: "pdf", description: "Portable Document Format"},
|
|
|
|
|
|
description: "Portable Document Format",
|
|
|
|
|
|
filename: "internal-pdf-viewer",
|
|
|
|
|
|
length: 1,
|
|
|
|
|
|
name: "Chrome PDF Plugin"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
0: {type: "application/pdf", suffixes: "pdf", description: ""},
|
|
|
|
|
|
description: "",
|
|
|
|
|
|
filename: "mhjfbmdgcfjbbpaeojofohoefgiehjai",
|
|
|
|
|
|
length: 1,
|
|
|
|
|
|
name: "Chrome PDF Viewer"
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 模拟permissions API
|
|
|
|
|
|
const originalQuery = window.navigator.permissions.query;
|
|
|
|
|
|
window.navigator.permissions.query = (parameters) => (
|
|
|
|
|
|
parameters.name === 'notifications' ?
|
|
|
|
|
|
Promise.resolve({ state: Notification.permission }) :
|
|
|
|
|
|
originalQuery(parameters)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 阻止检测自动化的网络请求
|
|
|
|
|
|
const originalFetch = window.fetch;
|
|
|
|
|
|
window.fetch = function(...args) {
|
|
|
|
|
|
const url = args[0];
|
|
|
|
|
|
if (typeof url === 'string' && (
|
|
|
|
|
|
url.includes('127.0.0.1:9222') ||
|
|
|
|
|
|
url.includes('localhost:9222') ||
|
|
|
|
|
|
url.includes('chrome-extension://invalid')
|
|
|
|
|
|
)) {
|
|
|
|
|
|
return Promise.reject(new Error('blocked'));
|
|
|
|
|
|
}
|
|
|
|
|
|
return originalFetch.apply(this, args);
|
|
|
|
|
|
};
|
|
|
|
|
|
""")
|
|
|
|
|
|
print("[临时浏览器] 已注入反检测脚本", file=sys.stderr)
|
|
|
|
|
|
|
2026-01-06 19:36:42 +08:00
|
|
|
|
# 注入Cookie
|
|
|
|
|
|
if cookies:
|
|
|
|
|
|
await context.add_cookies(cookies)
|
|
|
|
|
|
|
|
|
|
|
|
# 创建页面
|
|
|
|
|
|
page = await context.new_page()
|
|
|
|
|
|
|
|
|
|
|
|
return browser, context, page
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[临时浏览器] 创建失败: {str(e)}", file=sys.stderr)
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
async def release_temp_browser(self, session_id: str):
|
|
|
|
|
|
"""释放临时浏览器"""
|
|
|
|
|
|
async with self.temp_lock:
|
|
|
|
|
|
if session_id in self.temp_browsers:
|
|
|
|
|
|
browser_info = self.temp_browsers[session_id]
|
|
|
|
|
|
try:
|
|
|
|
|
|
await browser_info["page"].close()
|
|
|
|
|
|
await browser_info["context"].close()
|
|
|
|
|
|
await browser_info["browser"].close()
|
|
|
|
|
|
print(f"[浏览器池] 已释放会话 {session_id} 的临时浏览器", file=sys.stderr)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[浏览器池] 释放临时浏览器异常: {str(e)}", file=sys.stderr)
|
|
|
|
|
|
finally:
|
|
|
|
|
|
del self.temp_browsers[session_id]
|
|
|
|
|
|
|
2026-01-07 22:55:12 +08:00
|
|
|
|
async def get_qrcode_page(self, session_id: str) -> Page:
|
|
|
|
|
|
"""
|
|
|
|
|
|
为扫码登录获取页面(页面隔离模式)
|
|
|
|
|
|
多个用户共享同一个浏览器实例,但每个用户有独立的page
|
|
|
|
|
|
这样可以大大减少浏览器崩溃风险
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
session_id: 会话 ID
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Page 对象
|
|
|
|
|
|
"""
|
|
|
|
|
|
async with self.qrcode_lock:
|
|
|
|
|
|
# 复用已有的page
|
|
|
|
|
|
if session_id in self.qrcode_pages:
|
|
|
|
|
|
print(f"[扫码页面池] 复用会话 {session_id} 的页面", file=sys.stderr)
|
|
|
|
|
|
return self.qrcode_pages[session_id]["page"]
|
|
|
|
|
|
|
|
|
|
|
|
# 确保主浏览器已初始化
|
|
|
|
|
|
async with self.init_lock:
|
|
|
|
|
|
if not await self._is_browser_alive():
|
|
|
|
|
|
print("[扫码页面池] 主浏览器未初始化,创建中...", file=sys.stderr)
|
|
|
|
|
|
await self._init_browser()
|
|
|
|
|
|
|
|
|
|
|
|
# 从主context创建新page
|
|
|
|
|
|
print(f"[扫码页面池] 为会话 {session_id} 创建新页面 ({len(self.qrcode_pages)+1} 个活跃页面)", file=sys.stderr)
|
|
|
|
|
|
page = await self.context.new_page()
|
|
|
|
|
|
|
|
|
|
|
|
self.qrcode_pages[session_id] = {
|
|
|
|
|
|
"page": page,
|
|
|
|
|
|
"created_at": time.time()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return page
|
|
|
|
|
|
|
|
|
|
|
|
async def release_qrcode_page(self, session_id: str):
|
|
|
|
|
|
"""释放扫码登录页面"""
|
|
|
|
|
|
async with self.qrcode_lock:
|
|
|
|
|
|
if session_id in self.qrcode_pages:
|
|
|
|
|
|
page_info = self.qrcode_pages[session_id]
|
|
|
|
|
|
try:
|
|
|
|
|
|
await page_info["page"].close()
|
|
|
|
|
|
print(f"[扫码页面池] 已释放会话 {session_id} 的页面", file=sys.stderr)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[扫码页面池] 释放页面异常: {str(e)}", file=sys.stderr)
|
|
|
|
|
|
finally:
|
|
|
|
|
|
del self.qrcode_pages[session_id]
|
|
|
|
|
|
|
2026-01-06 19:36:42 +08:00
|
|
|
|
def get_stats(self) -> Dict[str, Any]:
|
|
|
|
|
|
"""获取浏览器池统计信息"""
|
|
|
|
|
|
return {
|
|
|
|
|
|
"browser_alive": self.browser is not None,
|
|
|
|
|
|
"context_alive": self.context is not None,
|
|
|
|
|
|
"page_alive": self.page is not None,
|
|
|
|
|
|
"is_preheated": self.is_preheated,
|
|
|
|
|
|
"temp_browsers_count": len(self.temp_browsers),
|
2026-01-07 22:55:12 +08:00
|
|
|
|
"qrcode_pages_count": len(self.qrcode_pages),
|
2026-01-06 19:36:42 +08:00
|
|
|
|
"max_instances": self.max_instances,
|
|
|
|
|
|
"last_used_time": self.last_used_time,
|
|
|
|
|
|
"idle_seconds": int(time.time() - self.last_used_time) if self.last_used_time > 0 else 0,
|
|
|
|
|
|
"idle_timeout": self.idle_timeout
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 全局单例
|
|
|
|
|
|
_browser_pool: Optional[BrowserPool] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_browser_pool(idle_timeout: int = 1800, headless: bool = True) -> BrowserPool:
|
|
|
|
|
|
"""获取全局浏览器池实例(单例)
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
idle_timeout: 空闲超时时间(秒)
|
|
|
|
|
|
headless: 是否使用无头模式,False为有头模式(方便调试)
|
|
|
|
|
|
"""
|
|
|
|
|
|
global _browser_pool
|
|
|
|
|
|
if _browser_pool is None:
|
|
|
|
|
|
print(f"[浏览器池] 创建单例,模式: {'headless' if headless else 'headed'}", file=sys.stderr)
|
|
|
|
|
|
_browser_pool = BrowserPool(idle_timeout=idle_timeout, headless=headless)
|
|
|
|
|
|
elif _browser_pool.headless != headless:
|
|
|
|
|
|
# 如果headless配置变了,需要更新
|
|
|
|
|
|
print(f"[浏览器池] 检测到headless配置变更: {_browser_pool.headless} -> {headless}", file=sys.stderr)
|
|
|
|
|
|
_browser_pool.headless = headless
|
|
|
|
|
|
return _browser_pool
|