stealth.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. # -*- coding: utf-8 -*-
  2. """
  3. Playwright Stealth Utils - 反检测工具
  4. 用于隐藏浏览器自动化特征,绕过平台风控
  5. """
  6. import random
  7. import hashlib
  8. import time
  9. from typing import Dict, Any, Optional
  10. # Stealth JS 脚本 - 隐藏 webdriver 等自动化特征
  11. STEALTH_JS = """
  12. // 1. 隐藏 webdriver 属性
  13. Object.defineProperty(navigator, 'webdriver', {
  14. get: () => undefined
  15. });
  16. // 2. 修改 plugins 长度(非零表示真实浏览器)
  17. Object.defineProperty(navigator, 'plugins', {
  18. get: () => {
  19. const plugins = [
  20. { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
  21. { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
  22. { name: 'Native Client', filename: 'internal-nacl-plugin', description: '' }
  23. ];
  24. plugins.item = (index) => plugins[index] || null;
  25. plugins.namedItem = (name) => plugins.find(p => p.name === name) || null;
  26. plugins.refresh = () => {};
  27. return plugins;
  28. }
  29. });
  30. // 3. 修改 languages
  31. Object.defineProperty(navigator, 'languages', {
  32. get: () => ['zh-CN', 'zh', 'en-US', 'en']
  33. });
  34. // 4. 隐藏自动化相关属性
  35. delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array;
  36. delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise;
  37. delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol;
  38. delete window.cdc_adoQpoasnfa76pfcZLmcfl_JSON;
  39. delete window.cdc_adoQpoasnfa76pfcZLmcfl_Object;
  40. // 5. 伪装 Chrome 属性
  41. window.chrome = {
  42. app: {
  43. isInstalled: false,
  44. InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' },
  45. RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' }
  46. },
  47. runtime: {
  48. OnInstalledReason: { CHROME_UPDATE: 'chrome_update', INSTALL: 'install', SHARED_MODULE_UPDATE: 'shared_module_update', UPDATE: 'update' },
  49. OnRestartRequiredReason: { APP_UPDATE: 'app_update', OS_UPDATE: 'os_update', PERIODIC: 'periodic' },
  50. PlatformArch: { ARM: 'arm', ARM64: 'arm64', MIPS: 'mips', MIPS64: 'mips64', X86_32: 'x86-32', X86_64: 'x86-64' },
  51. PlatformNaclArch: { ARM: 'arm', MIPS: 'mips', MIPS64: 'mips64', X86_32: 'x86-32', X86_64: 'x86-64' },
  52. PlatformOs: { ANDROID: 'android', CROS: 'cros', LINUX: 'linux', MAC: 'mac', OPENBSD: 'openbsd', WIN: 'win' },
  53. RequestUpdateCheckStatus: { NO_UPDATE: 'no_update', THROTTLED: 'throttled', UPDATE_AVAILABLE: 'update_available' },
  54. connect: function() { return { onDisconnect: { addListener: function() {} }, onMessage: { addListener: function() {} }, postMessage: function() {} }; },
  55. sendMessage: function() {}
  56. },
  57. csi: function() { return {}; },
  58. loadTimes: function() { return {}; }
  59. };
  60. // 6. 修复 permissions API
  61. const originalQuery = window.navigator.permissions?.query;
  62. if (originalQuery) {
  63. window.navigator.permissions.query = (parameters) => {
  64. if (parameters.name === 'notifications') {
  65. return Promise.resolve({ state: Notification.permission });
  66. }
  67. return originalQuery.call(window.navigator.permissions, parameters);
  68. };
  69. }
  70. // 7. 伪装 WebGL 指纹
  71. const getParameter = WebGLRenderingContext.prototype.getParameter;
  72. WebGLRenderingContext.prototype.getParameter = function(parameter) {
  73. if (parameter === 37445) {
  74. return 'Intel Inc.';
  75. }
  76. if (parameter === 37446) {
  77. return 'Intel Iris OpenGL Engine';
  78. }
  79. return getParameter.call(this, parameter);
  80. };
  81. // 8. 隐藏自动化 iframe
  82. const originalContentWindow = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentWindow').get;
  83. Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', {
  84. get: function() {
  85. const window = originalContentWindow.call(this);
  86. if (window) {
  87. try {
  88. Object.defineProperty(window.navigator, 'webdriver', { get: () => undefined });
  89. } catch (e) {}
  90. }
  91. return window;
  92. }
  93. });
  94. // 9. 伪装设备内存
  95. Object.defineProperty(navigator, 'deviceMemory', {
  96. get: () => 8
  97. });
  98. // 10. 伪装硬件并发数
  99. Object.defineProperty(navigator, 'hardwareConcurrency', {
  100. get: () => 8
  101. });
  102. // 11. 伪装电池 API
  103. if (navigator.getBattery) {
  104. const originalGetBattery = navigator.getBattery;
  105. navigator.getBattery = function() {
  106. return Promise.resolve({
  107. charging: true,
  108. chargingTime: 0,
  109. dischargingTime: Infinity,
  110. level: 1,
  111. addEventListener: function() {},
  112. removeEventListener: function() {},
  113. dispatchEvent: function() { return true; }
  114. });
  115. };
  116. }
  117. // 12. 修复 iframe contentWindow
  118. const originalAppendChild = Element.prototype.appendChild;
  119. Element.prototype.appendChild = function(node) {
  120. if (node.tagName === 'IFRAME') {
  121. try {
  122. Object.defineProperty(node, 'setAttribute', {
  123. value: function(name, value) {
  124. if (name === 'src' && value && value.includes('charset=utf-8')) {
  125. return;
  126. }
  127. return Element.prototype.setAttribute.call(this, name, value);
  128. }
  129. });
  130. } catch (e) {}
  131. }
  132. return originalAppendChild.call(this, node);
  133. };
  134. console.log('[Stealth] Anti-detection scripts loaded');
  135. """
  136. # Canvas 指纹噪声脚本
  137. CANVAS_NOISE_JS = """
  138. // Canvas 指纹噪声
  139. const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
  140. const originalToBlob = HTMLCanvasElement.prototype.toBlob;
  141. const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData;
  142. // 添加微小噪声
  143. function addNoise(data) {
  144. for (let i = 0; i < data.length; i += 4) {
  145. // 对 RGBA 添加微小随机噪声(-1 到 1)
  146. data[i] = Math.max(0, Math.min(255, data[i] + Math.floor(Math.random() * 3) - 1));
  147. data[i + 1] = Math.max(0, Math.min(255, data[i + 1] + Math.floor(Math.random() * 3) - 1));
  148. data[i + 2] = Math.max(0, Math.min(255, data[i + 2] + Math.floor(Math.random() * 3) - 1));
  149. }
  150. return data;
  151. }
  152. HTMLCanvasElement.prototype.toDataURL = function(type) {
  153. if (this.width > 0 && this.height > 0) {
  154. try {
  155. const ctx = this.getContext('2d');
  156. if (ctx) {
  157. const imageData = ctx.getImageData(0, 0, this.width, this.height);
  158. addNoise(imageData.data);
  159. ctx.putImageData(imageData, 0, 0);
  160. }
  161. } catch (e) {}
  162. }
  163. return originalToDataURL.apply(this, arguments);
  164. };
  165. CanvasRenderingContext2D.prototype.getImageData = function() {
  166. const imageData = originalGetImageData.apply(this, arguments);
  167. addNoise(imageData.data);
  168. return imageData;
  169. };
  170. """
  171. # Audio 指纹噪声脚本
  172. AUDIO_NOISE_JS = """
  173. // Audio 指纹噪声
  174. const originalCreateAnalyser = AudioContext.prototype.createAnalyser;
  175. const originalGetFloatFrequencyData = AnalyserNode.prototype.getFloatFrequencyData;
  176. AudioContext.prototype.createAnalyser = function() {
  177. const analyser = originalCreateAnalyser.call(this);
  178. const originalGetFloatFrequencyData = analyser.getFloatFrequencyData.bind(analyser);
  179. analyser.getFloatFrequencyData = function(array) {
  180. originalGetFloatFrequencyData(array);
  181. for (let i = 0; i < array.length; i++) {
  182. array[i] += (Math.random() - 0.5) * 0.001;
  183. }
  184. };
  185. return analyser;
  186. };
  187. """
  188. def get_user_agent() -> str:
  189. """获取随机 User-Agent"""
  190. user_agents = [
  191. # Chrome on Windows
  192. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
  193. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
  194. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
  195. # Chrome on Mac
  196. "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",
  197. "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",
  198. # Edge on Windows
  199. "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",
  200. ]
  201. return random.choice(user_agents)
  202. def get_viewport() -> Dict[str, int]:
  203. """获取随机视口大小"""
  204. viewports = [
  205. {"width": 1920, "height": 1080},
  206. {"width": 1680, "height": 1050},
  207. {"width": 1440, "height": 900},
  208. {"width": 1536, "height": 864},
  209. {"width": 1366, "height": 768},
  210. {"width": 2560, "height": 1440},
  211. ]
  212. return random.choice(viewports)
  213. def get_timezone() -> str:
  214. """获取时区"""
  215. return "Asia/Shanghai"
  216. def get_locale() -> str:
  217. """获取语言区域"""
  218. return "zh-CN"
  219. def get_screen_info() -> Dict[str, int]:
  220. """获取屏幕信息"""
  221. screens = [
  222. {"screen_width": 1920, "screen_height": 1080, "device_pixel_ratio": 1},
  223. {"screen_width": 2560, "screen_height": 1440, "device_pixel_ratio": 1},
  224. {"screen_width": 1680, "screen_height": 1050, "device_pixel_ratio": 1},
  225. {"screen_width": 1440, "screen_height": 900, "device_pixel_ratio": 2}, # Retina
  226. ]
  227. return random.choice(screens)
  228. def get_webgl_vendor_renderer() -> Dict[str, str]:
  229. """获取 WebGL 供应商和渲染器"""
  230. configs = [
  231. {"vendor": "Google Inc. (Intel)", "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 630, OpenGL 4.6)"},
  232. {"vendor": "Google Inc. (NVIDIA)", "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 1060, OpenGL 4.6)"},
  233. {"vendor": "Google Inc. (AMD)", "renderer": "ANGLE (AMD, AMD Radeon RX 580, OpenGL 4.6)"},
  234. {"vendor": "Intel Inc.", "renderer": "Intel Iris OpenGL Engine"},
  235. ]
  236. return random.choice(configs)
  237. def get_browser_context_args(
  238. proxy_config: Optional[Dict[str, Any]] = None,
  239. user_agent: Optional[str] = None,
  240. viewport: Optional[Dict[str, int]] = None,
  241. ) -> Dict[str, Any]:
  242. """
  243. 生成浏览器上下文参数
  244. Args:
  245. proxy_config: 代理配置 {"server": "http://ip:port"}
  246. user_agent: 自定义 User-Agent
  247. viewport: 视口大小 {"width": 1920, "height": 1080}
  248. Returns:
  249. 浏览器上下文参数字典
  250. """
  251. screen_info = get_screen_info()
  252. args = {
  253. "user_agent": user_agent or get_user_agent(),
  254. "viewport": viewport or get_viewport(),
  255. "locale": get_locale(),
  256. "timezone_id": get_timezone(),
  257. "geolocation": {"latitude": 31.2304, "longitude": 121.4737}, # 上海
  258. "permissions": ["geolocation"],
  259. "color_scheme": "light",
  260. "device_scale_factor": screen_info.get("device_pixel_ratio", 1),
  261. "has_touch": False,
  262. "is_mobile": False,
  263. "java_script_enabled": True,
  264. "ignore_https_errors": True,
  265. }
  266. if proxy_config and proxy_config.get("server"):
  267. args["proxy"] = proxy_config
  268. return args
  269. def get_stealth_scripts() -> str:
  270. """获取所有反检测脚本"""
  271. return f"""
  272. {STEALTH_JS}
  273. {CANVAS_NOISE_JS}
  274. {AUDIO_NOISE_JS}
  275. """
  276. def get_webgl_override_script() -> str:
  277. """获取 WebGL 指纹覆盖脚本"""
  278. webgl_config = get_webgl_vendor_renderer()
  279. return f"""
  280. // WebGL 指纹覆盖
  281. const getParameter = WebGLRenderingContext.prototype.getParameter;
  282. WebGLRenderingContext.prototype.getParameter = function(parameter) {{
  283. // UNMASKED_VENDOR_WEBGL
  284. if (parameter === 37445) {{
  285. return '{webgl_config["vendor"]}';
  286. }}
  287. // UNMASKED_RENDERER_WEBGL
  288. if (parameter === 37446) {{
  289. return '{webgl_config["renderer"]}';
  290. }}
  291. return getParameter.call(this, parameter);
  292. }};
  293. // WebGL2 支持同样需要覆盖
  294. if (typeof WebGL2RenderingContext !== 'undefined') {{
  295. const getParameter2 = WebGL2RenderingContext.prototype.getParameter;
  296. WebGL2RenderingContext.prototype.getParameter = function(parameter) {{
  297. if (parameter === 37445) {{
  298. return '{webgl_config["vendor"]}';
  299. }}
  300. if (parameter === 37446) {{
  301. return '{webgl_config["renderer"]}';
  302. }}
  303. return getParameter2.call(this, parameter);
  304. }};
  305. }}
  306. """
  307. def get_navigator_override_script() -> str:
  308. """获取 Navigator 属性覆盖脚本"""
  309. screen_info = get_screen_info()
  310. return f"""
  311. // Navigator 属性覆盖
  312. Object.defineProperty(screen, 'width', {{
  313. get: () => {screen_info['screen_width']}
  314. }});
  315. Object.defineProperty(screen, 'height', {{
  316. get: () => {screen_info['screen_height']}
  317. }});
  318. Object.defineProperty(screen, 'availWidth', {{
  319. get: () => {screen_info['screen_width']}
  320. }});
  321. Object.defineProperty(screen, 'availHeight', {{
  322. get: () => {screen_info['screen_height'] - 40} // 减去任务栏高度
  323. }});
  324. Object.defineProperty(window, 'devicePixelRatio', {{
  325. get: () => {screen_info.get('device_pixel_ratio', 1)}
  326. }});
  327. """
  328. def get_all_stealth_scripts() -> str:
  329. """获取完整的反检测脚本组合"""
  330. return f"""
  331. {get_stealth_scripts()}
  332. {get_webgl_override_script()}
  333. {get_navigator_override_script()}
  334. // 最终确认
  335. console.log('[Stealth] All anti-detection scripts loaded successfully');
  336. """
  337. async def inject_stealth_scripts(page) -> None:
  338. """
  339. 向页面注入反检测脚本
  340. Args:
  341. page: Playwright page 对象
  342. """
  343. stealth_script = get_all_stealth_scripts()
  344. # 在页面加载前注入
  345. await page.add_init_script(stealth_script)
  346. # 如果页面已经加载,立即执行
  347. try:
  348. await page.evaluate(stealth_script)
  349. except Exception as e:
  350. # 页面可能还未完全加载,忽略错误
  351. pass
  352. async def human_like_delay(min_ms: int = 100, max_ms: int = 500) -> None:
  353. """
  354. 模拟人类操作的延迟
  355. Args:
  356. min_ms: 最小延迟毫秒数
  357. max_ms: 最大延迟毫秒数
  358. """
  359. delay = random.randint(min_ms, max_ms) / 1000.0
  360. time.sleep(delay)
  361. async def human_like_type(page, selector: str, text: str, delay_range: tuple = (50, 150)) -> None:
  362. """
  363. 模拟人类输入
  364. Args:
  365. page: Playwright page 对象
  366. selector: 选择器
  367. text: 要输入的文本
  368. delay_range: 延迟范围(毫秒)
  369. """
  370. element = page.locator(selector)
  371. await element.click()
  372. for char in text:
  373. await element.press(char)
  374. delay = random.randint(delay_range[0], delay_range[1]) / 1000.0
  375. time.sleep(delay)
  376. async def human_like_scroll(page, distance: int = None) -> None:
  377. """
  378. 模拟人类滚动
  379. Args:
  380. page: Playwright page 对象
  381. distance: 滚动距离(像素),默认随机
  382. """
  383. if distance is None:
  384. distance = random.randint(200, 500)
  385. # 分多次滚动,模拟真实行为
  386. steps = random.randint(3, 6)
  387. step_distance = distance // steps
  388. for _ in range(steps):
  389. await page.evaluate(f"window.scrollBy(0, {step_distance})")
  390. await human_like_delay(100, 300)