You've already forked pruchase_jhw
first commit
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
from playwright.async_api import async_playwright, Playwright, BrowserContext
|
||||
from loguru import logger
|
||||
from src.config.settings import get_settings
|
||||
from src.config.accounts import AccountConfig
|
||||
|
||||
# 注入到所有页面的 JS 脚本,用于绕过 Cloudflare、Akamai 等自动化检测
|
||||
STEALTH_JS = """
|
||||
// 1. 隐藏 navigator.webdriver
|
||||
Object.defineProperty(navigator, 'webdriver', {
|
||||
get: () => undefined
|
||||
});
|
||||
|
||||
// 2. 覆盖语言列表,优先日语(与目标地区一致)
|
||||
Object.defineProperty(navigator, 'languages', {
|
||||
get: () => ['ja-JP', 'ja', 'en-US', 'en']
|
||||
});
|
||||
|
||||
// 3. 伪造 WebGL 渲染器信息,伪装为标准硬件
|
||||
const getParameter = WebGLRenderingContext.prototype.getParameter;
|
||||
WebGLRenderingContext.prototype.getParameter = function(parameter) {
|
||||
// UNMASKED_VENDOR_WEBGL (37445)
|
||||
if (parameter === 37445) {
|
||||
return 'Intel Inc.';
|
||||
}
|
||||
// UNMASKED_RENDERER_WEBGL (37446)
|
||||
if (parameter === 37446) {
|
||||
return 'Intel(R) UHD Graphics';
|
||||
}
|
||||
return getParameter.apply(this, arguments);
|
||||
};
|
||||
|
||||
// 4. 伪造设备规格
|
||||
Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 });
|
||||
Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });
|
||||
|
||||
// 5. 模拟完整插件列表(无头浏览器常见特征为空列表)
|
||||
const mockPlugins = () => {
|
||||
const makePlugin = (name, filename, description) => {
|
||||
const p = Object.create(Plugin.prototype);
|
||||
Object.defineProperties(p, {
|
||||
name: { get: () => name },
|
||||
filename: { get: () => filename },
|
||||
description: { get: () => description },
|
||||
length: { get: () => 0 }
|
||||
});
|
||||
return p;
|
||||
};
|
||||
const plugins = [
|
||||
makePlugin("PDF Viewer", "internal-pdf-viewer", "Portable Document Format"),
|
||||
makePlugin("Chrome PDF Viewer", "mhjfbgoacfdegokjbbpboihdaibdccjg", "Chrome PDF Viewer"),
|
||||
makePlugin("Chromium PDF Viewer", "internal-pdf-viewer", "Chromium PDF Viewer")
|
||||
];
|
||||
const pList = Object.create(PluginArray.prototype);
|
||||
Object.defineProperties(pList, {
|
||||
length: { get: () => plugins.length },
|
||||
item: { value: (index) => plugins[index] },
|
||||
namedItem: { value: (name) => plugins.find(p => p.name === name) }
|
||||
});
|
||||
for (let i = 0; i < plugins.length; i++) {
|
||||
Object.defineProperty(pList, i, { get: () => plugins[i] });
|
||||
}
|
||||
return pList;
|
||||
};
|
||||
Object.defineProperty(navigator, 'plugins', {
|
||||
get: () => mockPlugins()
|
||||
});
|
||||
|
||||
// 6. 模拟 window.chrome(无头浏览器通常没有,Cloudflare 会检测)
|
||||
window.chrome = {
|
||||
app: {
|
||||
isInstalled: false,
|
||||
InstallState: {
|
||||
DISABLED: 'disabled',
|
||||
INSTALLED: 'installed',
|
||||
NOT_INSTALLED: 'not_installed'
|
||||
},
|
||||
RunningState: {
|
||||
CANNOT_RUN: 'cannot_run',
|
||||
READY_TO_RUN: 'ready_to_run',
|
||||
RUNNING: 'running'
|
||||
}
|
||||
},
|
||||
runtime: {
|
||||
OnInstalledReason: {
|
||||
CHROME_UPDATE: 'chrome_update',
|
||||
INSTALL: 'install',
|
||||
SHARED_MODULE_UPDATE: 'shared_module_update',
|
||||
UPDATE: 'update'
|
||||
},
|
||||
OnRestartRequiredReason: {
|
||||
APP_UPDATE: 'app_update',
|
||||
OS_UPDATE: 'os_update',
|
||||
PERIODIC: 'periodic'
|
||||
},
|
||||
PlatformArch: {
|
||||
ARM: 'arm',
|
||||
ARM64: 'arm64',
|
||||
MIPS: 'mips',
|
||||
MIPS64: 'mips64',
|
||||
X86_32: 'x86-32',
|
||||
X86_64: 'x86-64'
|
||||
},
|
||||
PlatformNaclArch: {
|
||||
ARM: 'arm',
|
||||
MIPS: 'mips',
|
||||
MIPS64: 'mips64',
|
||||
X86_32: 'x86-32',
|
||||
X86_64: 'x86-64'
|
||||
},
|
||||
PlatformOs: {
|
||||
ANDROID: 'android',
|
||||
CROS: 'cros',
|
||||
LINUX: 'linux',
|
||||
MAC: 'mac',
|
||||
OPENBSD: 'openbsd',
|
||||
WIN: 'win'
|
||||
},
|
||||
RequestUpdateCheckStatus: {
|
||||
NO_UPDATE: 'no_update',
|
||||
THROTTLED: 'throttled',
|
||||
UPDATE_AVAILABLE: 'update_available'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 7. 模拟 Permissions 查询行为一致性
|
||||
const originalQuery = window.navigator.permissions.query;
|
||||
window.navigator.permissions.query = (parameters) => (
|
||||
parameters.name === 'notifications' ?
|
||||
Promise.resolve({ state: Notification.permission }) :
|
||||
originalQuery(parameters)
|
||||
);
|
||||
"""
|
||||
|
||||
class BrowserManager:
|
||||
"""
|
||||
浏览器管理器:负责初始化 Playwright 上下文。
|
||||
按账号隔离数据环境(user_data_dir),并注入反爬脚本以规避 Cloudflare、Akamai 等检测。
|
||||
"""
|
||||
def __init__(self) -> None:
|
||||
self.settings = get_settings()
|
||||
|
||||
async def get_context(self, account: AccountConfig) -> Tuple[Playwright, BrowserContext]:
|
||||
"""
|
||||
为指定账号启动持久化浏览器上下文。
|
||||
返回 (Playwright, BrowserContext) 元组。
|
||||
调用方必须在 try-finally 中确保关闭 context 与 playwright 实例。
|
||||
|
||||
Args:
|
||||
account: 包含 account_id、user_data_dir 及可选 proxy 的账号配置。
|
||||
|
||||
Returns:
|
||||
Tuple[Playwright, BrowserContext]: 已启动的驱动与隔离浏览器上下文实例。
|
||||
"""
|
||||
# 1. 解析并确保本地持久化目录存在
|
||||
data_dir = Path(account.user_data_dir)
|
||||
try:
|
||||
data_dir.mkdir(parents=True, exist_ok=True)
|
||||
# logger.debug(f"持久化用户数据目录: {data_dir.absolute()}")
|
||||
except Exception as e:
|
||||
logger.error(f"创建账号 '{account.account_id}' 的持久化目录失败: {e}")
|
||||
raise e
|
||||
|
||||
# 2. 配置隐身 Chromium 启动参数
|
||||
chrome_args = [
|
||||
"--disable-blink-features=AutomationControlled", # 浏览器层面移除 navigator.webdriver
|
||||
"--disable-infobars", # 隐藏「Chrome 正受到自动测试软件控制」提示条
|
||||
"--no-sandbox", # Docker/服务器环境兼容
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage", # 内存受限环境避免资源崩溃
|
||||
"--start-maximized", # 启动时全屏(最大化)
|
||||
"--disable-gpu", # 服务器无头模式稳定渲染
|
||||
"--use-mock-keychain", # 避免本地密钥环弹窗(macOS/Linux)
|
||||
"--password-store=basic",
|
||||
]
|
||||
|
||||
executable_path_raw = self.settings.browser_executable_path
|
||||
executable_path = None
|
||||
if executable_path_raw:
|
||||
candidate_path = Path(executable_path_raw).expanduser()
|
||||
if candidate_path.is_file():
|
||||
executable_path = str(candidate_path)
|
||||
logger.info(f"使用自定义浏览器可执行文件: {executable_path}")
|
||||
else:
|
||||
logger.warning(
|
||||
f"配置的浏览器可执行文件无效,将使用 Playwright 默认浏览器: {executable_path_raw}"
|
||||
)
|
||||
headless_mode = self.settings.browser_headless
|
||||
|
||||
# 使用较新的 Windows Chrome User-Agent
|
||||
user_agent = (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
||||
)
|
||||
|
||||
logger.info(f"正在为账号 {account.account_id} 启动 Playwright | 无头模式: {headless_mode}")
|
||||
|
||||
# 3. 按账号配置可选代理(服务器 IP 段绕过 Cloudflare 封锁时常用)
|
||||
proxy_config = None
|
||||
if account.proxy:
|
||||
proxy_config = {"server": account.proxy}
|
||||
logger.info(f"账号 '{account.account_id}' 已启用代理: {account.proxy}")
|
||||
|
||||
playwright_driver = await async_playwright().start()
|
||||
|
||||
try:
|
||||
# 4. 使用隔离目录创建持久化上下文
|
||||
launch_options = {
|
||||
"user_data_dir": str(data_dir.absolute()),
|
||||
"headless": headless_mode,
|
||||
"args": chrome_args,
|
||||
"user_agent": user_agent,
|
||||
"no_viewport": True, # 配合 --start-maximized 使得网页视口也铺满全屏
|
||||
"locale": "ja-JP", # 区域语言一致
|
||||
"timezone_id": "Asia/Tokyo", # 日本本地时区
|
||||
"accept_downloads": True, # 允许自动下载(如需要)
|
||||
"proxy": proxy_config # 住宅代理等
|
||||
}
|
||||
if executable_path:
|
||||
launch_options["executable_path"] = executable_path
|
||||
|
||||
context = await playwright_driver.chromium.launch_persistent_context(**launch_options)
|
||||
|
||||
# 5. 注入高级反指纹隐身脚本
|
||||
await context.add_init_script(STEALTH_JS)
|
||||
logger.success(f"账号 '{account.account_id}' 的 Playwright 上下文启动成功。")
|
||||
|
||||
return playwright_driver, context
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"账号 '{account.account_id}' 启动持久化上下文失败: {e}")
|
||||
# 启动失败时清理驱动进程
|
||||
await playwright_driver.stop()
|
||||
raise e
|
||||
Reference in New Issue
Block a user