|
|
@@ -0,0 +1,461 @@
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
+"""
|
|
|
+Playwright Stealth Utils - 反检测工具
|
|
|
+用于隐藏浏览器自动化特征,绕过平台风控
|
|
|
+"""
|
|
|
+
|
|
|
+import random
|
|
|
+import hashlib
|
|
|
+import time
|
|
|
+from typing import Dict, Any, Optional
|
|
|
+
|
|
|
+
|
|
|
+# Stealth JS 脚本 - 隐藏 webdriver 等自动化特征
|
|
|
+STEALTH_JS = """
|
|
|
+// 1. 隐藏 webdriver 属性
|
|
|
+Object.defineProperty(navigator, 'webdriver', {
|
|
|
+ get: () => undefined
|
|
|
+});
|
|
|
+
|
|
|
+// 2. 修改 plugins 长度(非零表示真实浏览器)
|
|
|
+Object.defineProperty(navigator, 'plugins', {
|
|
|
+ get: () => {
|
|
|
+ const plugins = [
|
|
|
+ { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
|
|
|
+ { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
|
|
|
+ { name: 'Native Client', filename: 'internal-nacl-plugin', description: '' }
|
|
|
+ ];
|
|
|
+ plugins.item = (index) => plugins[index] || null;
|
|
|
+ plugins.namedItem = (name) => plugins.find(p => p.name === name) || null;
|
|
|
+ plugins.refresh = () => {};
|
|
|
+ return plugins;
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// 3. 修改 languages
|
|
|
+Object.defineProperty(navigator, 'languages', {
|
|
|
+ get: () => ['zh-CN', 'zh', 'en-US', 'en']
|
|
|
+});
|
|
|
+
|
|
|
+// 4. 隐藏自动化相关属性
|
|
|
+delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array;
|
|
|
+delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise;
|
|
|
+delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol;
|
|
|
+delete window.cdc_adoQpoasnfa76pfcZLmcfl_JSON;
|
|
|
+delete window.cdc_adoQpoasnfa76pfcZLmcfl_Object;
|
|
|
+
|
|
|
+// 5. 伪装 Chrome 属性
|
|
|
+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' },
|
|
|
+ connect: function() { return { onDisconnect: { addListener: function() {} }, onMessage: { addListener: function() {} }, postMessage: function() {} }; },
|
|
|
+ sendMessage: function() {}
|
|
|
+ },
|
|
|
+ csi: function() { return {}; },
|
|
|
+ loadTimes: function() { return {}; }
|
|
|
+};
|
|
|
+
|
|
|
+// 6. 修复 permissions API
|
|
|
+const originalQuery = window.navigator.permissions?.query;
|
|
|
+if (originalQuery) {
|
|
|
+ window.navigator.permissions.query = (parameters) => {
|
|
|
+ if (parameters.name === 'notifications') {
|
|
|
+ return Promise.resolve({ state: Notification.permission });
|
|
|
+ }
|
|
|
+ return originalQuery.call(window.navigator.permissions, parameters);
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+// 7. 伪装 WebGL 指纹
|
|
|
+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);
|
|
|
+};
|
|
|
+
|
|
|
+// 8. 隐藏自动化 iframe
|
|
|
+const originalContentWindow = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentWindow').get;
|
|
|
+Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', {
|
|
|
+ get: function() {
|
|
|
+ const window = originalContentWindow.call(this);
|
|
|
+ if (window) {
|
|
|
+ try {
|
|
|
+ Object.defineProperty(window.navigator, 'webdriver', { get: () => undefined });
|
|
|
+ } catch (e) {}
|
|
|
+ }
|
|
|
+ return window;
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// 9. 伪装设备内存
|
|
|
+Object.defineProperty(navigator, 'deviceMemory', {
|
|
|
+ get: () => 8
|
|
|
+});
|
|
|
+
|
|
|
+// 10. 伪装硬件并发数
|
|
|
+Object.defineProperty(navigator, 'hardwareConcurrency', {
|
|
|
+ get: () => 8
|
|
|
+});
|
|
|
+
|
|
|
+// 11. 伪装电池 API
|
|
|
+if (navigator.getBattery) {
|
|
|
+ const originalGetBattery = navigator.getBattery;
|
|
|
+ navigator.getBattery = function() {
|
|
|
+ return Promise.resolve({
|
|
|
+ charging: true,
|
|
|
+ chargingTime: 0,
|
|
|
+ dischargingTime: Infinity,
|
|
|
+ level: 1,
|
|
|
+ addEventListener: function() {},
|
|
|
+ removeEventListener: function() {},
|
|
|
+ dispatchEvent: function() { return true; }
|
|
|
+ });
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+// 12. 修复 iframe contentWindow
|
|
|
+const originalAppendChild = Element.prototype.appendChild;
|
|
|
+Element.prototype.appendChild = function(node) {
|
|
|
+ if (node.tagName === 'IFRAME') {
|
|
|
+ try {
|
|
|
+ Object.defineProperty(node, 'setAttribute', {
|
|
|
+ value: function(name, value) {
|
|
|
+ if (name === 'src' && value && value.includes('charset=utf-8')) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ return Element.prototype.setAttribute.call(this, name, value);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } catch (e) {}
|
|
|
+ }
|
|
|
+ return originalAppendChild.call(this, node);
|
|
|
+};
|
|
|
+
|
|
|
+console.log('[Stealth] Anti-detection scripts loaded');
|
|
|
+"""
|
|
|
+
|
|
|
+# Canvas 指纹噪声脚本
|
|
|
+CANVAS_NOISE_JS = """
|
|
|
+// Canvas 指纹噪声
|
|
|
+const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
|
|
+const originalToBlob = HTMLCanvasElement.prototype.toBlob;
|
|
|
+const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData;
|
|
|
+
|
|
|
+// 添加微小噪声
|
|
|
+function addNoise(data) {
|
|
|
+ for (let i = 0; i < data.length; i += 4) {
|
|
|
+ // 对 RGBA 添加微小随机噪声(-1 到 1)
|
|
|
+ data[i] = Math.max(0, Math.min(255, data[i] + Math.floor(Math.random() * 3) - 1));
|
|
|
+ data[i + 1] = Math.max(0, Math.min(255, data[i + 1] + Math.floor(Math.random() * 3) - 1));
|
|
|
+ data[i + 2] = Math.max(0, Math.min(255, data[i + 2] + Math.floor(Math.random() * 3) - 1));
|
|
|
+ }
|
|
|
+ return data;
|
|
|
+}
|
|
|
+
|
|
|
+HTMLCanvasElement.prototype.toDataURL = function(type) {
|
|
|
+ if (this.width > 0 && this.height > 0) {
|
|
|
+ try {
|
|
|
+ const ctx = this.getContext('2d');
|
|
|
+ if (ctx) {
|
|
|
+ const imageData = ctx.getImageData(0, 0, this.width, this.height);
|
|
|
+ addNoise(imageData.data);
|
|
|
+ ctx.putImageData(imageData, 0, 0);
|
|
|
+ }
|
|
|
+ } catch (e) {}
|
|
|
+ }
|
|
|
+ return originalToDataURL.apply(this, arguments);
|
|
|
+};
|
|
|
+
|
|
|
+CanvasRenderingContext2D.prototype.getImageData = function() {
|
|
|
+ const imageData = originalGetImageData.apply(this, arguments);
|
|
|
+ addNoise(imageData.data);
|
|
|
+ return imageData;
|
|
|
+};
|
|
|
+"""
|
|
|
+
|
|
|
+# Audio 指纹噪声脚本
|
|
|
+AUDIO_NOISE_JS = """
|
|
|
+// Audio 指纹噪声
|
|
|
+const originalCreateAnalyser = AudioContext.prototype.createAnalyser;
|
|
|
+const originalGetFloatFrequencyData = AnalyserNode.prototype.getFloatFrequencyData;
|
|
|
+
|
|
|
+AudioContext.prototype.createAnalyser = function() {
|
|
|
+ const analyser = originalCreateAnalyser.call(this);
|
|
|
+ const originalGetFloatFrequencyData = analyser.getFloatFrequencyData.bind(analyser);
|
|
|
+ analyser.getFloatFrequencyData = function(array) {
|
|
|
+ originalGetFloatFrequencyData(array);
|
|
|
+ for (let i = 0; i < array.length; i++) {
|
|
|
+ array[i] += (Math.random() - 0.5) * 0.001;
|
|
|
+ }
|
|
|
+ };
|
|
|
+ return analyser;
|
|
|
+};
|
|
|
+"""
|
|
|
+
|
|
|
+
|
|
|
+def get_user_agent() -> str:
|
|
|
+ """获取随机 User-Agent"""
|
|
|
+ user_agents = [
|
|
|
+ # Chrome on Windows
|
|
|
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
|
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
|
|
|
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
|
|
+ # Chrome on Mac
|
|
|
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
|
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
|
|
|
+ # Edge on Windows
|
|
|
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
|
|
|
+ ]
|
|
|
+ return random.choice(user_agents)
|
|
|
+
|
|
|
+
|
|
|
+def get_viewport() -> Dict[str, int]:
|
|
|
+ """获取随机视口大小"""
|
|
|
+ viewports = [
|
|
|
+ {"width": 1920, "height": 1080},
|
|
|
+ {"width": 1680, "height": 1050},
|
|
|
+ {"width": 1440, "height": 900},
|
|
|
+ {"width": 1536, "height": 864},
|
|
|
+ {"width": 1366, "height": 768},
|
|
|
+ {"width": 2560, "height": 1440},
|
|
|
+ ]
|
|
|
+ return random.choice(viewports)
|
|
|
+
|
|
|
+
|
|
|
+def get_timezone() -> str:
|
|
|
+ """获取时区"""
|
|
|
+ return "Asia/Shanghai"
|
|
|
+
|
|
|
+
|
|
|
+def get_locale() -> str:
|
|
|
+ """获取语言区域"""
|
|
|
+ return "zh-CN"
|
|
|
+
|
|
|
+
|
|
|
+def get_screen_info() -> Dict[str, int]:
|
|
|
+ """获取屏幕信息"""
|
|
|
+ screens = [
|
|
|
+ {"screen_width": 1920, "screen_height": 1080, "device_pixel_ratio": 1},
|
|
|
+ {"screen_width": 2560, "screen_height": 1440, "device_pixel_ratio": 1},
|
|
|
+ {"screen_width": 1680, "screen_height": 1050, "device_pixel_ratio": 1},
|
|
|
+ {"screen_width": 1440, "screen_height": 900, "device_pixel_ratio": 2}, # Retina
|
|
|
+ ]
|
|
|
+ return random.choice(screens)
|
|
|
+
|
|
|
+
|
|
|
+def get_webgl_vendor_renderer() -> Dict[str, str]:
|
|
|
+ """获取 WebGL 供应商和渲染器"""
|
|
|
+ configs = [
|
|
|
+ {"vendor": "Google Inc. (Intel)", "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 630, OpenGL 4.6)"},
|
|
|
+ {"vendor": "Google Inc. (NVIDIA)", "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 1060, OpenGL 4.6)"},
|
|
|
+ {"vendor": "Google Inc. (AMD)", "renderer": "ANGLE (AMD, AMD Radeon RX 580, OpenGL 4.6)"},
|
|
|
+ {"vendor": "Intel Inc.", "renderer": "Intel Iris OpenGL Engine"},
|
|
|
+ ]
|
|
|
+ return random.choice(configs)
|
|
|
+
|
|
|
+
|
|
|
+def get_browser_context_args(
|
|
|
+ proxy_config: Optional[Dict[str, Any]] = None,
|
|
|
+ user_agent: Optional[str] = None,
|
|
|
+ viewport: Optional[Dict[str, int]] = None,
|
|
|
+) -> Dict[str, Any]:
|
|
|
+ """
|
|
|
+ 生成浏览器上下文参数
|
|
|
+
|
|
|
+ Args:
|
|
|
+ proxy_config: 代理配置 {"server": "http://ip:port"}
|
|
|
+ user_agent: 自定义 User-Agent
|
|
|
+ viewport: 视口大小 {"width": 1920, "height": 1080}
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ 浏览器上下文参数字典
|
|
|
+ """
|
|
|
+ screen_info = get_screen_info()
|
|
|
+
|
|
|
+ args = {
|
|
|
+ "user_agent": user_agent or get_user_agent(),
|
|
|
+ "viewport": viewport or get_viewport(),
|
|
|
+ "locale": get_locale(),
|
|
|
+ "timezone_id": get_timezone(),
|
|
|
+ "geolocation": {"latitude": 31.2304, "longitude": 121.4737}, # 上海
|
|
|
+ "permissions": ["geolocation"],
|
|
|
+ "color_scheme": "light",
|
|
|
+ "device_scale_factor": screen_info.get("device_pixel_ratio", 1),
|
|
|
+ "has_touch": False,
|
|
|
+ "is_mobile": False,
|
|
|
+ "java_script_enabled": True,
|
|
|
+ "ignore_https_errors": True,
|
|
|
+ }
|
|
|
+
|
|
|
+ if proxy_config and proxy_config.get("server"):
|
|
|
+ args["proxy"] = proxy_config
|
|
|
+
|
|
|
+ return args
|
|
|
+
|
|
|
+
|
|
|
+def get_stealth_scripts() -> str:
|
|
|
+ """获取所有反检测脚本"""
|
|
|
+ return f"""
|
|
|
+{STEALTH_JS}
|
|
|
+
|
|
|
+{CANVAS_NOISE_JS}
|
|
|
+
|
|
|
+{AUDIO_NOISE_JS}
|
|
|
+"""
|
|
|
+
|
|
|
+
|
|
|
+def get_webgl_override_script() -> str:
|
|
|
+ """获取 WebGL 指纹覆盖脚本"""
|
|
|
+ webgl_config = get_webgl_vendor_renderer()
|
|
|
+ return f"""
|
|
|
+// WebGL 指纹覆盖
|
|
|
+const getParameter = WebGLRenderingContext.prototype.getParameter;
|
|
|
+WebGLRenderingContext.prototype.getParameter = function(parameter) {{
|
|
|
+ // UNMASKED_VENDOR_WEBGL
|
|
|
+ if (parameter === 37445) {{
|
|
|
+ return '{webgl_config["vendor"]}';
|
|
|
+ }}
|
|
|
+ // UNMASKED_RENDERER_WEBGL
|
|
|
+ if (parameter === 37446) {{
|
|
|
+ return '{webgl_config["renderer"]}';
|
|
|
+ }}
|
|
|
+ return getParameter.call(this, parameter);
|
|
|
+}};
|
|
|
+
|
|
|
+// WebGL2 支持同样需要覆盖
|
|
|
+if (typeof WebGL2RenderingContext !== 'undefined') {{
|
|
|
+ const getParameter2 = WebGL2RenderingContext.prototype.getParameter;
|
|
|
+ WebGL2RenderingContext.prototype.getParameter = function(parameter) {{
|
|
|
+ if (parameter === 37445) {{
|
|
|
+ return '{webgl_config["vendor"]}';
|
|
|
+ }}
|
|
|
+ if (parameter === 37446) {{
|
|
|
+ return '{webgl_config["renderer"]}';
|
|
|
+ }}
|
|
|
+ return getParameter2.call(this, parameter);
|
|
|
+ }};
|
|
|
+}}
|
|
|
+"""
|
|
|
+
|
|
|
+
|
|
|
+def get_navigator_override_script() -> str:
|
|
|
+ """获取 Navigator 属性覆盖脚本"""
|
|
|
+ screen_info = get_screen_info()
|
|
|
+ return f"""
|
|
|
+// Navigator 属性覆盖
|
|
|
+Object.defineProperty(screen, 'width', {{
|
|
|
+ get: () => {screen_info['screen_width']}
|
|
|
+}});
|
|
|
+Object.defineProperty(screen, 'height', {{
|
|
|
+ get: () => {screen_info['screen_height']}
|
|
|
+}});
|
|
|
+Object.defineProperty(screen, 'availWidth', {{
|
|
|
+ get: () => {screen_info['screen_width']}
|
|
|
+}});
|
|
|
+Object.defineProperty(screen, 'availHeight', {{
|
|
|
+ get: () => {screen_info['screen_height'] - 40} // 减去任务栏高度
|
|
|
+}});
|
|
|
+Object.defineProperty(window, 'devicePixelRatio', {{
|
|
|
+ get: () => {screen_info.get('device_pixel_ratio', 1)}
|
|
|
+}});
|
|
|
+"""
|
|
|
+
|
|
|
+
|
|
|
+def get_all_stealth_scripts() -> str:
|
|
|
+ """获取完整的反检测脚本组合"""
|
|
|
+ return f"""
|
|
|
+{get_stealth_scripts()}
|
|
|
+
|
|
|
+{get_webgl_override_script()}
|
|
|
+
|
|
|
+{get_navigator_override_script()}
|
|
|
+
|
|
|
+// 最终确认
|
|
|
+console.log('[Stealth] All anti-detection scripts loaded successfully');
|
|
|
+"""
|
|
|
+
|
|
|
+
|
|
|
+async def inject_stealth_scripts(page) -> None:
|
|
|
+ """
|
|
|
+ 向页面注入反检测脚本
|
|
|
+
|
|
|
+ Args:
|
|
|
+ page: Playwright page 对象
|
|
|
+ """
|
|
|
+ stealth_script = get_all_stealth_scripts()
|
|
|
+
|
|
|
+ # 在页面加载前注入
|
|
|
+ await page.add_init_script(stealth_script)
|
|
|
+
|
|
|
+ # 如果页面已经加载,立即执行
|
|
|
+ try:
|
|
|
+ await page.evaluate(stealth_script)
|
|
|
+ except Exception as e:
|
|
|
+ # 页面可能还未完全加载,忽略错误
|
|
|
+ pass
|
|
|
+
|
|
|
+
|
|
|
+async def human_like_delay(min_ms: int = 100, max_ms: int = 500) -> None:
|
|
|
+ """
|
|
|
+ 模拟人类操作的延迟
|
|
|
+
|
|
|
+ Args:
|
|
|
+ min_ms: 最小延迟毫秒数
|
|
|
+ max_ms: 最大延迟毫秒数
|
|
|
+ """
|
|
|
+ delay = random.randint(min_ms, max_ms) / 1000.0
|
|
|
+ time.sleep(delay)
|
|
|
+
|
|
|
+
|
|
|
+async def human_like_type(page, selector: str, text: str, delay_range: tuple = (50, 150)) -> None:
|
|
|
+ """
|
|
|
+ 模拟人类输入
|
|
|
+
|
|
|
+ Args:
|
|
|
+ page: Playwright page 对象
|
|
|
+ selector: 选择器
|
|
|
+ text: 要输入的文本
|
|
|
+ delay_range: 延迟范围(毫秒)
|
|
|
+ """
|
|
|
+ element = page.locator(selector)
|
|
|
+ await element.click()
|
|
|
+
|
|
|
+ for char in text:
|
|
|
+ await element.press(char)
|
|
|
+ delay = random.randint(delay_range[0], delay_range[1]) / 1000.0
|
|
|
+ time.sleep(delay)
|
|
|
+
|
|
|
+
|
|
|
+async def human_like_scroll(page, distance: int = None) -> None:
|
|
|
+ """
|
|
|
+ 模拟人类滚动
|
|
|
+
|
|
|
+ Args:
|
|
|
+ page: Playwright page 对象
|
|
|
+ distance: 滚动距离(像素),默认随机
|
|
|
+ """
|
|
|
+ if distance is None:
|
|
|
+ distance = random.randint(200, 500)
|
|
|
+
|
|
|
+ # 分多次滚动,模拟真实行为
|
|
|
+ steps = random.randint(3, 6)
|
|
|
+ step_distance = distance // steps
|
|
|
+
|
|
|
+ for _ in range(steps):
|
|
|
+ await page.evaluate(f"window.scrollBy(0, {step_distance})")
|
|
|
+ await human_like_delay(100, 300)
|