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