# -*- 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)