zy

perf: 浏览器反检测模块全面优化 - 动态平台适配与指纹一致性强化

核心改进:

1. stealth.py 架构升级:
   - UA 自动检测平台(macOS/Windows/Linux)替代硬编码
   - 新增 Client Hints (userAgentMetadata) 覆盖,防止 navigator.userAgentData 泄露
   - navigator.webdriver 改用 Proxy 包装原生 getter,通过 toString() 检测仍返回 [native code]
   - 移除 plugins 覆盖(真实 Chrome 已有正确 PluginArray)
   - 新增 chrome.app 对象和 navigator.vendor="Google Inc."
   - WebGL 同时覆盖 WebGL1 和 WebGL2,vendor/renderer 与平台一致
   - 移除 outerWidth/outerHeight 覆盖(headed 模式下设为相等反而暴露自动化)
   - build_ua_override() 函数支持动态 Chrome 版本注入

2. cdp.py 集成:
   - connect() 从 /json/version 提取真实 Chrome 版本号
   - _setup_page() 使用动态版本构建 UA 和 Client Hints

技术要点:
- 所有信号(UA/platform/Client Hints/WebGL)跨模块一致性保证
- 兼容 macOS (M1/Intel)、Windows、Linux 三平台
- 动态版本匹配确保与实际 Chrome 实例同步

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: zy <xpzouying@gmail.com>
... ... @@ -15,7 +15,7 @@ import requests
import websockets.sync.client as ws_client
from .errors import CDPError, ElementNotFoundError
from .stealth import REALISTIC_UA, STEALTH_JS
from .stealth import STEALTH_JS, build_ua_override
logger = logging.getLogger(__name__)
... ... @@ -571,6 +571,7 @@ class Browser:
self.port = port
self.base_url = f"http://{host}:{port}"
self._cdp: CDPClient | None = None
self._chrome_version: str | None = None
def connect(self) -> None:
"""连接到 Chrome DevTools。"""
... ... @@ -578,7 +579,13 @@ class Browser:
resp.raise_for_status()
info = resp.json()
ws_url = info["webSocketDebuggerUrl"]
logger.info("连接到 Chrome: %s", ws_url)
# 从 "Chrome/134.0.6998.88" 提取真实版本号,用于动态构建 UA
browser_str = info.get("Browser", "")
if "/" in browser_str:
self._chrome_version = browser_str.split("/", 1)[1]
logger.info("连接到 Chrome: %s (version=%s)", ws_url, self._chrome_version)
self._cdp = CDPClient(ws_url)
def _setup_page(self, page: Page) -> Page:
... ... @@ -586,7 +593,10 @@ class Browser:
import contextlib
page.inject_stealth()
page._send_session("Emulation.setUserAgentOverride", {"userAgent": REALISTIC_UA})
page._send_session(
"Emulation.setUserAgentOverride",
build_ua_override(self._chrome_version),
)
page._send_session(
"Emulation.setDeviceMetricsOverride",
{
... ...
"""反检测 JS 注入 + Chrome 启动参数,对应 go-rod/stealth。"""
"""反检测配置:UA / Client Hints / JS 注入 / Chrome 启动参数。
# 真实 Chrome UA(固定版本,避免每次随机导致指纹不一致)
REALISTIC_UA = (
关键原则:UA、navigator.platform、Client Hints、WebGL 等所有信号必须与实际平台一致。
"""
from __future__ import annotations
import platform as _platform
# Chrome 版本号 — 定期更新以匹配主流版本(当前对应 2025 年中期稳定版)
_CHROME_VER = "136"
_CHROME_FULL_VER = "136.0.0.0"
def _build_platform_config() -> dict:
"""根据实际操作系统生成一致的 UA / Client Hints / WebGL 配置。"""
system = _platform.system()
brands = [
{"brand": "Chromium", "version": _CHROME_VER},
{"brand": "Google Chrome", "version": _CHROME_VER},
{"brand": "Not-A.Brand", "version": "24"},
]
full_version_list = [
{"brand": "Chromium", "version": _CHROME_FULL_VER},
{"brand": "Google Chrome", "version": _CHROME_FULL_VER},
{"brand": "Not-A.Brand", "version": "24.0.0.0"},
]
if system == "Darwin":
arch = "arm" if _platform.machine() == "arm64" else "x86"
return {
"ua": (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
f"Chrome/{_CHROME_FULL_VER} Safari/537.36"
),
"nav_platform": "MacIntel",
"ua_metadata": {
"brands": brands,
"fullVersionList": full_version_list,
"platform": "macOS",
"platformVersion": "14.5.0",
"architecture": arch,
"model": "",
"mobile": False,
"bitness": "64",
"wow64": False,
},
"webgl_vendor": "Apple Inc.",
"webgl_renderer": (
"ANGLE (Apple, ANGLE Metal Renderer: Apple M1, Unspecified Version)"
),
}
if system == "Windows":
return {
"ua": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/131.0.0.0 Safari/537.36"
)
f"Chrome/{_CHROME_FULL_VER} Safari/537.36"
),
"nav_platform": "Win32",
"ua_metadata": {
"brands": brands,
"fullVersionList": full_version_list,
"platform": "Windows",
"platformVersion": "15.0.0",
"architecture": "x86",
"model": "",
"mobile": False,
"bitness": "64",
"wow64": False,
},
"webgl_vendor": "Google Inc. (Intel)",
"webgl_renderer": (
"ANGLE (Intel, Intel(R) UHD Graphics 630 (CML GT2), Direct3D11)"
),
}
# Linux
return {
"ua": (
"Mozilla/5.0 (X11; Linux x86_64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
f"Chrome/{_CHROME_FULL_VER} Safari/537.36"
),
"nav_platform": "Linux x86_64",
"ua_metadata": {
"brands": brands,
"fullVersionList": full_version_list,
"platform": "Linux",
"platformVersion": "6.5.0",
"architecture": "x86",
"model": "",
"mobile": False,
"bitness": "64",
"wow64": False,
},
"webgl_vendor": "Google Inc. (Mesa)",
"webgl_renderer": (
"ANGLE (Mesa, Mesa Intel(R) UHD Graphics 630 (CML GT2), OpenGL 4.6)"
),
}
PLATFORM_CONFIG = _build_platform_config()
# 向后兼容导出
REALISTIC_UA = PLATFORM_CONFIG["ua"]
def build_ua_override(chrome_full_ver: str | None = None) -> dict:
"""构建 Emulation.setUserAgentOverride 参数。
Args:
chrome_full_ver: Chrome 完整版本号(如 "134.0.6998.88"),
从 CDP /json/version 接口获取。为 None 时使用默认值。
Returns:
可直接传给 Emulation.setUserAgentOverride 的参数字典。
"""
ver = chrome_full_ver or _CHROME_FULL_VER
major = ver.split(".")[0]
system = _platform.system()
brands = [
{"brand": "Chromium", "version": major},
{"brand": "Google Chrome", "version": major},
{"brand": "Not-A.Brand", "version": "24"},
]
full_version_list = [
{"brand": "Chromium", "version": ver},
{"brand": "Google Chrome", "version": ver},
{"brand": "Not-A.Brand", "version": "24.0.0.0"},
]
# 反检测 JS 脚本:在页面加载时注入
STEALTH_JS = """
if system == "Darwin":
arch = "arm" if _platform.machine() == "arm64" else "x86"
ua = (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
f"Chrome/{ver} Safari/537.36"
)
nav_platform = "MacIntel"
ua_platform = "macOS"
platform_ver = "14.5.0"
elif system == "Windows":
arch = "x86"
ua = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
f"Chrome/{ver} Safari/537.36"
)
nav_platform = "Win32"
ua_platform = "Windows"
platform_ver = "15.0.0"
else:
arch = "x86"
ua = (
"Mozilla/5.0 (X11; Linux x86_64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
f"Chrome/{ver} Safari/537.36"
)
nav_platform = "Linux x86_64"
ua_platform = "Linux"
platform_ver = "6.5.0"
return {
"userAgent": ua,
"platform": nav_platform,
"userAgentMetadata": {
"brands": brands,
"fullVersionList": full_version_list,
"platform": ua_platform,
"platformVersion": platform_ver,
"architecture": arch,
"model": "",
"mobile": False,
"bitness": "64",
"wow64": False,
},
}
# ---------------------------------------------------------------------------
# 反检测 JS 脚本模板($$占位符$$ 由 Python 替换为平台值)
# ---------------------------------------------------------------------------
_STEALTH_JS_TEMPLATE = """
(() => {
// 1. navigator.webdriver
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined,
// 1. navigator.webdriver — Proxy 包装原始 native getter,toString() 仍返回 [native code]
const wd = Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver');
if (wd && wd.get) {
Object.defineProperty(Navigator.prototype, 'webdriver', {
get: new Proxy(wd.get, { apply: () => false }),
configurable: true,
});
}
// 2. chrome.runtime
if (!window.chrome) {
window.chrome = {};
}
if (!window.chrome) window.chrome = {};
if (!window.chrome.runtime) {
window.chrome.runtime = {
connect: () => {},
sendMessage: () => {},
};
window.chrome.runtime = { connect: () => {}, sendMessage: () => {} };
}
// 3. plugins
Object.defineProperty(navigator, 'plugins', {
get: () => {
return [
{
0: {type: 'application/x-google-chrome-pdf'},
description: 'Portable Document Format',
filename: 'internal-pdf-viewer',
length: 1,
name: 'Chrome PDF Plugin',
},
{
0: {type: 'application/pdf'},
description: '',
filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai',
length: 1,
name: 'Chrome PDF Viewer',
},
{
0: {type: 'application/x-nacl'},
description: '',
filename: 'internal-nacl-plugin',
length: 1,
name: 'Native Client',
// 3. chrome.app — headless 缺失此对象,检测脚本会检查
if (!window.chrome.app) {
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',
},
getDetails: function() {},
getIsInstalled: function() {},
installState: function() { return 'not_installed'; },
runningState: function() { return 'cannot_run'; },
};
}
// 4. navigator.vendor — Chrome 应返回 "Google Inc."
Object.defineProperty(navigator, 'vendor', {
get: () => 'Google Inc.',
configurable: true,
});
// 5. plugins — 不覆盖,真实 Chrome 已有正确的 PluginArray
// 4. languages
Object.defineProperty(navigator, 'languages', {
get: () => ['zh-CN', 'zh', 'en-US', 'en'],
... ... @@ -72,13 +246,19 @@ STEALTH_JS = """
: originalQuery(parameters);
}
// 6. WebGL vendor/renderer
const getParameter = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(parameter) {
if (parameter === 37445) return 'Intel Inc.';
if (parameter === 37446) return 'Intel Iris OpenGL Engine';
return getParameter.call(this, parameter);
// 6. WebGL vendor/renderer — 与平台一致(同时覆盖 WebGL1 和 WebGL2)
const overrideWebGL = (proto) => {
const original = proto.getParameter;
proto.getParameter = function(p) {
if (p === 37445) return '$$WEBGL_VENDOR$$';
if (p === 37446) return '$$WEBGL_RENDERER$$';
return original.call(this, p);
};
};
overrideWebGL(WebGLRenderingContext.prototype);
if (typeof WebGL2RenderingContext !== 'undefined') {
overrideWebGL(WebGL2RenderingContext.prototype);
}
// 7. hardwareConcurrency — 随机 4 或 8
Object.defineProperty(navigator, 'hardwareConcurrency', {
... ... @@ -109,18 +289,18 @@ STEALTH_JS = """
window.chrome.loadTimes = function() { return {}; };
}
// 11. outerWidth/outerHeight — 与 innerWidth/innerHeight 对齐
Object.defineProperty(window, 'outerWidth', {
get: () => window.innerWidth,
configurable: true,
});
Object.defineProperty(window, 'outerHeight', {
get: () => window.innerHeight,
configurable: true,
});
// 11. outerWidth/outerHeight — 不覆盖
// 正常浏览器 outer > inner(有标题栏/工具栏),设为相等反而暴露自动化特征
})();
"""
STEALTH_JS = (
_STEALTH_JS_TEMPLATE
.replace("$$WEBGL_VENDOR$$", PLATFORM_CONFIG["webgl_vendor"])
.replace("$$WEBGL_RENDERER$$", PLATFORM_CONFIG["webgl_renderer"])
)
# Chrome 启动参数(反检测相关)
STEALTH_ARGS = [
"--disable-blink-features=AutomationControlled",
... ...