当前位置: 首页 > news >正文

Selenium JS注入实战:绕过动态Token、Canvas指纹与行为检测

1. 为什么光靠click()和send_keys()永远卡在登录页我第一次用Selenium写电商比价脚本时信心满满地写了二十行代码打开首页、定位搜索框、输入关键词、点击搜索按钮——结果页面始终停留在加载状态控制台里连个Network请求都没发出去。反复检查XPath确认元素存在加了显式等待确认按钮可点击甚至手动点一遍流程完全没问题。最后抓包才发现那个“搜索”按钮根本不是传统表单提交而是绑定了一个JS事件监听器触发时会先调用window._trackSearch()埋点再异步调用fetch(/api/search)拉数据。而Selenium的.click()只模拟了DOM点击行为压根没触发绑定在onclick或addEventListener上的JS逻辑。这就是绝大多数新手爬虫卡死的真相你面对的不是静态HTML而是一个运行中的JavaScript应用。现代网站90%以上的交互逻辑都由前端框架Vue/React/Angular驱动按钮点击、下拉加载、验证码校验、防刷token生成……全靠JS运行时动态控制。Selenium本身只是浏览器自动化工具不是JS引擎——它能操作DOM但不自动执行页面上那些“悄悄话”般的JS逻辑。所谓“突破反爬”本质不是对抗某个加密算法而是让自动化脚本真正像人一样在正确的时机、以正确的上下文执行那一段关键JS代码。这个标题里的“手把手”不是教你怎么写driver.execute_script(return document.title)这种Hello World而是带你拆解真实业务场景中必须绕过的三类JS关卡动态Token生成如每次请求前计算时间戳随机数签名、Canvas指纹混淆通过JS读取Canvas像素值伪造设备ID、行为轨迹模拟用JS注入鼠标移动路径绕过“非人类操作”检测。接下来每一节我都用自己踩过的坑、修过的bug、压测过的参数告诉你怎么把JS代码精准“打进去”而不是盲目execute_script乱试。2. Selenium执行JS的底层机制不是调用而是注入很多人以为execute_script()是“调用页面JS函数”这是个危险误解。实际上Selenium的JS执行机制更接近“代码注入”——它把你的字符串代码作为一段独立脚本插入到当前页面的执行上下文中运行。这带来三个关键事实直接决定你能不能绕过反爬2.1 执行上下文隔离你的JS和页面JS不在同一个“房间”当你写driver.execute_script(console.log(window.myGlobalVar))这段代码是在Selenium创建的独立执行环境里运行的它能访问window对象但无法直接访问页面JS模块化作用域里的变量。比如页面用Webpack打包把tokenGenerator函数放在一个闭包里// 页面源码简化 (function() { const secretKey a1b2c3; window.generateToken function() { return md5(Date.now() Math.random() secretKey); }; })();此时driver.execute_script(generateToken())会报ReferenceError: generateToken is not defined因为generateToken只在那个匿名函数的闭包里可见没挂到全局window上。提示真正的解决方案不是“找全局变量”而是用arguments传参方式把页面函数“拽出来”。正确写法token driver.execute_script( // 把闭包里的函数暴露给全局仅本次执行有效 if (typeof window.__temp_generateToken undefined) { window.__temp_generateToken window.generateToken; } return window.__temp_generateToken(); )2.2 同步阻塞 vs 异步穿透为什么execute_script等不到PromiseSelenium的JS执行默认是同步阻塞的。你写result driver.execute_script( fetch(/api/data).then(res res.json()).then(data data.id) ) print(result) # 输出 None这段代码实际返回undefined因为execute_script只等待JS代码执行完即Promise对象创建完成不等待Promise内部的异步链。它就像按了播放键就立刻退出根本不管视频播没播完。要拿到异步结果必须用return显式返回一个Promise并让Selenium“穿透”等待result driver.execute_script( // 注意必须return一个Promise且不能用await老版本ChromeDriver不支持 return fetch(/api/data) .then(res res.json()) .then(data data.id); ) # 此时result才是真实的data.id值注意ChromeDriver 75才支持Promise穿透低于此版本需改用回调模式result driver.execute_script( let done arguments[0]; fetch(/api/data).then(res res.json()).then(data done(data.id)); )2.3 DOM就绪时机document.readyState不是万能钥匙很多教程说“等document.readyState complete再执行JS”但在SPA单页应用里这毫无意义。Vue应用可能DOM早已complete但Vue实例还没mountedthis.$refs.xxx仍是undefinedReact应用同理useEffect钩子没跑完数据根本没渲染。真实项目中我用的判断逻辑是三级等待基础层document.readyState complete确保HTML解析完毕框架层检测框架全局对象是否存在且就绪Vuereturn typeof Vue ! undefined Vue.nextTickReactreturn typeof React ! undefined React.version业务层等待具体业务节点出现并可交互WebDriverWait(driver, 10).until( lambda d: d.execute_script(return document.querySelector(#search-btn) ! null document.querySelector(#search-btn).offsetParent ! null) )这三层缺一不可。去年爬某招聘网站时我就栽在第二层——页面显示“加载中”但document.readyState已是completeVue对象也存在唯独Vue.nextTick没触发导致JS注入后取到的DOM节点全是空的。3. 突破动态Token关卡从逆向分析到实时生成几乎所有需要登录态的网站都会在请求头里加一个动态Token如X-Token、Authorization这个Token通常由JS实时生成且带有时效性5分钟过期、唯一性绑定设备指纹、不可预测性含随机盐值。想绕过别想着抓包复用得让Selenium“学会”生成它。3.1 定位Token生成函数三步逆向法以某金融平台为例其API请求头包含X-Signature: sha256(timestampnoncebodysecret)。第一步不是看网络请求而是打开开发者工具→Sources→Page→搜索X-Signature找到设置该header的代码// 源码片段 function signRequest(url, method, body) { const timestamp Date.now().toString(); const nonce Math.random().toString(36).substr(2, 8); const secret window._config.secretKey; // 关键secretKey从哪来 const str ${timestamp}${nonce}${JSON.stringify(body)}${secret}; return { X-Timestamp: timestamp, X-Nonce: nonce, X-Signature: CryptoJS.SHA256(str).toString() }; }第二步追踪_config.secretKey来源在Console里执行window._config发现它是通过fetch(/api/config)异步加载的。这意味着Token生成依赖前置请求。第三步验证函数可用性在Console里手动调用signRequest(/api/user, GET, {})确认返回值格式与抓包一致。实操心得别信“全局搜索JS文件”现代前端代码都是动态import的。正确姿势是在Network面板过滤XHR找到首次加载配置的请求通常是/api/config或/js/config.js在该请求的Initiator列点开看到调用栈顺藤摸瓜找到JS入口文件在Sources里用CtrlShiftF全局搜索X-Signature或sign等关键词比盲目翻文件快10倍3.2 构建可复用的JS注入模板把上面分析出的逻辑封装成Selenium可直接执行的JS字符串。注意三点参数化用arguments接收Python传入的参数避免字符串拼接XSS风险错误兜底JS执行失败时返回明确错误方便Python层捕获上下文兼容适配不同框架的全局对象如Vue的__vue__、React的__reactFiber最终模板如下def generate_signature(driver, url, method, body): js_code // 参数解构 const [urlArg, methodArg, bodyArg] arguments; // 检查依赖是否就绪 if (typeof window._config undefined || !window._config.secretKey) { throw new Error(Config not loaded: _config.secretKey missing); } if (typeof CryptoJS undefined) { throw new Error(CryptoJS not loaded); } // 生成签名 const timestamp Date.now().toString(); const nonce Math.random().toString(36).substr(2, 8); const secret window._config.secretKey; const str ${timestamp}${nonce}${JSON.stringify(bodyArg)}${secret}; return { X-Timestamp: timestamp, X-Nonce: nonce, X-Signature: CryptoJS.SHA256(str).toString(), X-Url: urlArg, X-Method: methodArg }; try: return driver.execute_script(js_code, url, method, body) except JavascriptException as e: raise RuntimeError(fJS signature generation failed: {e})调用时headers generate_signature(driver, /api/user, GET, {}) requests.get(https://api.example.com/user, headersheaders)3.3 处理Token时效性缓存策略与自动刷新动态Token通常5-10分钟过期如果脚本运行超时必须重新生成。我的方案是内存缓存用functools.lru_cache缓存最近一次生成结果带maxsize1和typedTrue时间戳校验在JS里同时返回timestampPython层判断是否超时自动刷新钩子封装一个get_api_data()方法内部自动检测Token过期并重试from functools import lru_cache import time lru_cache(maxsize1, typedTrue) def cached_signature(url, method, body): # ... 执行JS生成逻辑 return signature_dict def get_api_data(driver, url, methodGET, bodyNone): sig cached_signature(url, method, body or {}) # 检查timestamp是否超5分钟 if time.time() - int(sig[X-Timestamp]) 300: cached_signature.cache_clear() # 清除缓存强制刷新 sig cached_signature(url, method, body or {}) return requests.request(method, fhttps://api.example.com{url}, headerssig, jsonbody)去年爬某券商APP行情接口时就是靠这套缓存时间戳校验让脚本连续运行36小时无中断。中间遇到一次CDN缓存导致_config.secretKey更新cached_signature自动失效重试比手动监控日志省心太多。4. 绕过Canvas指纹检测用JS读取像素值伪造设备ID越来越多的反爬系统如极验、数美不再只看User-Agent而是用Canvas指纹——让JS在Canvas上画一段文字或图形再用getImageData()读取像素值生成唯一哈希。这个哈希值比IP还稳定同一台电脑每次都是同一个值而Selenium默认的ChromeDriver会暴露“自动化工具”特征返回固定假值如全黑画布。4.1 验证Canvas指纹是否启用不是所有网站都用Canvas指纹先确认是否真被检测。在目标页面Console执行// 创建临时Canvas测试 const canvas document.createElement(canvas); const ctx canvas.getContext(2d); ctx.textBaseline top; ctx.font 14px Arial; ctx.textRendering optimizeLegibility; ctx.fillText(Browser, 2, 2); const data ctx.getImageData(0, 0, 10, 10).data; console.log(Fingerprint hash:, Array.from(data).reduce((a, b) a ^ b, 0));如果多次执行返回相同数字说明Canvas指纹已启用如果返回0或随机值可能是未启用或被干扰。4.2 Selenium的Canvas“破绽”在哪ChromeDriver的Canvas实现有两大硬伤字体渲染差异无头模式下缺失系统字体Arial渲染成方块像素值全0抗锯齿关闭默认禁用imageSmoothingEnabled导致文字边缘锯齿哈希值与真人浏览器不同我在Chrome 115实测同一段JS在真人浏览器返回哈希12345在Selenium里返回67890差值高达5位数。4.3 用JS注入修复Canvas指纹核心思路在Selenium启动后立即注入一段JS覆盖Canvas的getImageData方法返回预生成的真实哈希对应像素值。步骤分三步第一步采集真人浏览器的Canvas像素数据用一台干净的Mac电脑字体最全打开目标页面执行上述测试代码记录data数组长度10x10x4400字节。保存为canvas_fingerprint.json{ width: 10, height: 10, data: [255,255,255,255, 0,0,0,255, ...] }第二步注入JS劫持getImageDatadef inject_canvas_fingerprint(driver, fingerprint_path): with open(fingerprint_path, r) as f: fp_data json.load(f) js_inject f // 保存原始方法 const originalGetImageData CanvasRenderingContext2D.prototype.getImageData; // 覆盖方法 CanvasRenderingContext2D.prototype.getImageData function(x, y, width, height) {{ // 只劫持我们关心的尺寸10x10 if (width {fp_data[width]} height {fp_data[height]}) {{ const fakeData new Uint8ClampedArray({fp_data[data]}); return {{ width: {fp_data[width]}, height: {fp_data[height]}, data: fakeData, // 必须实现data属性的getter否则某些库报错 get data() {{ return this._data; }}, set data(val) {{ this._data val; }} }}; }} // 其他尺寸走原逻辑 return originalGetImageData.call(this, x, y, width, height); }}; driver.execute_script(js_inject)第三步启动时自动注入在创建Driver实例后立即调用driver webdriver.Chrome(optionschrome_options) inject_canvas_fingerprint(driver, canvas_fingerprint.json) # 后续所有Canvas操作都返回真实指纹注意事项必须在页面加载任何JS之前注入否则网站JS可能已缓存原始getImageData引用fingerprint.json需定期更新建议每月重采因网站可能更换Canvas绘制逻辑如果网站用WebGL而非2D Canvas需额外劫持readPixels方法原理相同但参数更复杂去年爬某知识付费平台时他们用Canvas指纹WebGL指纹双重校验。我用同样方法劫持WebGLRenderingContext.prototype.readPixels把预存的WebGL像素值返回成功绕过。关键点在于反爬不是技术竞赛而是信息战——你只要比对方多知道一行JS代码的调用时机就能赢。5. 模拟人类行为轨迹用JS注入鼠标移动路径当网站发现你的鼠标从坐标(0,0)瞬间跳到(100,200)就知道是机器人。真实用户会有加速度、停顿、微小抖动。Selenium的ActionChains只能做直线移动而高级反爬如阿里系的神策、腾讯的防水墙会分析鼠标轨迹的贝塞尔曲线曲率识别出“机械运动”。5.1 解析真实鼠标轨迹数据我用录屏工具OBS录下自己搜索商品的全过程再用OpenCV提取鼠标坐标序列得到类似这样的数据t0ms: (100, 200) t120ms: (102, 201) # 微小偏移 t250ms: (110, 205) # 加速 t380ms: (130, 215) # 持续移动 t420ms: (130, 215) # 停顿悬停 t500ms: (132, 216) # 微调 ...导出为mouse_path.json格式为[ {x: 100, y: 200, delay: 0}, {x: 102, y: 201, delay: 120}, {x: 110, y: 205, delay: 250}, ... ]5.2 用JS注入实现贝塞尔平滑移动Selenium的move_to_element_with_offset()是瞬移我们要的是“人眼可见的移动过程”。方案是用JS在页面里创建一个div idfake-cursor作为虚拟鼠标用requestAnimationFrame按贝塞尔曲线插值移动它同时用dispatchEvent模拟真实的mousemove事件。注入JS代码def inject_mouse_trajectory(driver, path_file): with open(path_file, r) as f: path_data json.load(f) # 转换为JS数组字面量 js_path [ ,.join([ f{{x:{p[x]},y:{p[y]},delay:{p[delay]}} for p in path_data ]) ] js_code f // 创建虚拟鼠标 const cursor document.createElement(div); cursor.id fake-cursor; cursor.style.cssText position:fixed;width:10px;height:10px;background:red;border-radius:50%;pointer-events:none;z-index:9999;; document.body.appendChild(cursor); // 模拟mousemove事件 function dispatchMouseMove(x, y) {{ const event new MouseEvent(mousemove, {{ view: window, bubbles: true, cancelable: true, clientX: x, clientY: y }}); document.elementFromPoint(x, y)?.dispatchEvent(event) || document.dispatchEvent(event); }} // 执行轨迹 const path {js_path}; let startTime performance.now(); function animate(index) {{ if (index path.length) return; const point path[index]; const elapsed performance.now() - startTime; const targetTime point.delay; if (elapsed targetTime) {{ cursor.style.left (point.x - 5) px; cursor.style.top (point.y - 5) px; dispatchMouseMove(point.x, point.y); animate(index 1); }} else {{ requestAnimationFrame(() animate(index)); }} }} animate(0); driver.execute_script(js_code)5.3 与Selenium动作链协同工作注入JS只是“画皮”真正操作还得靠Selenium。我的做法是前期准备用JS注入轨迹让页面认为“用户已在移动”关键操作在轨迹到达目标元素附近时如delay值接近目标时间用Selenium执行.click()后期收尾JS继续播放剩余轨迹模拟点击后的自然移动# 注入轨迹提前执行 inject_mouse_trajectory(driver, mouse_path.json) # 等待轨迹播放到目标区域例如延迟800ms后到达搜索框 time.sleep(0.8) # 此时JS已把虚拟鼠标移到搜索框Selenium再点击 search_box driver.find_element(By.ID, search-input) search_box.click() search_box.send_keys(iPhone 15)这套组合拳在某跨境电商平台实测通过率从12%提升到93%。关键是JS负责“欺骗感知”Selenium负责“执行动作”两者分工明确互不干扰。别试图用JS去click()那只是自欺欺人——反爬系统早把document.elementFromPoint()的调用链监控死了。6. 实战排错为什么我的JS执行总是返回None这是新手最高频的问题。execute_script()返回None不是Bug而是设计如此——它只返回JS代码最后一行的执行结果。如果你最后一行是console.log()或if语句必然返回None。6.1 返回值陷阱排查清单现象根本原因修复方案总是None最后一行是console.log()、var x1;、if(){}等无返回值语句强制添加returnreturn document.title;返回undefined访问了不存在的属性如window.xxx.yyy或函数没return用??操作符兜底return window.xxx?.yyy ?? default;返回空字符串JS里用了alert()或prompt()但Selenium不支持弹窗彻底删除所有UI交互JS改用console.log调试返回[object Promise]忘了returnPromise或ChromeDriver版本太低升级ChromeDriver至115或改用回调模式6.2 调试JS执行的黄金三步法第一步在Chrome DevTools Console里100%复现把你要注入的JS代码完整粘贴到目标页面Console执行确认返回值正确。这是铁律——如果Console里都跑不通注入到Selenium里必死。第二步用console.log打桩查看执行路径在JS代码关键位置加console.log然后用Selenium的get_log(browser)获取日志# 注入带log的JS driver.execute_script( console.log(Step 1: config loaded?, window._config); console.log(Step 2: secret key length, window._config?.secretKey?.length); return window._config?.secretKey; ) # 获取日志 for entry in driver.get_log(browser): print(entry) # 查看JS是否执行到某一步第三步检查ChromeDriver与Chrome版本匹配常见坑Chrome 120需要ChromeDriver 120.0.6099.109版本不匹配会导致execute_script静默失败。用以下命令验证google-chrome --version # 查看Chrome版本 chromedriver --version # 查看ChromeDriver版本不匹配去https://chromedriver.chromium.org/下载对应版本。6.3 生产环境避坑指南超时设置execute_script默认无超时JS死循环会让脚本卡死。务必用set_script_timeout()driver.set_script_timeout(10) # 10秒超时沙箱冲突某些网站启用了sandboxiframeJS无法跨域执行。解决方案是切换到目标iframedriver.switch_to.frame(driver.find_element(By.TAG_NAME, iframe)) result driver.execute_script(return document.body.innerHTML) driver.switch_to.default_content()CSP限制网站Content-Security-Policy禁止unsafe-eval导致JS注入失败。此时只能改用execute_async_script配合回调或放弃JS方案改用其他反爬策略。最后分享个血泪教训去年爬某政务平台所有JS注入都返回None折腾三天才发现是网站启用了strict-dynamicCSP策略连内联script都不让执行。最终方案是——不用JS改用requests直接调APISelenium只负责登录拿Cookie。有时候最优雅的解决方案就是承认JS注入不适用及时转向。
http://www.gsyq.cn/news/1396107.html

相关文章:

  • 从零搭建Lovable保险系统,手把手实现监管沙盒对接、实时核保引擎与客户情感化交互模块
  • PersistentWindows:解决Windows多显示器窗口管理难题的智能助手
  • 2026 年 Ai 呼叫系统哪家靠谱:云蝠智能大众信赖 - 17329971652
  • 2026 年外呼机器人哪家强:云蝠智能冠绝业内 - 13425704091
  • ArchR实战避坑指南:从scATAC-seq原始数据到细胞轨迹分析,我的完整复盘与参数调优心得
  • Unity WebGL截图下载完整方案:从GPU读取到Blob URL下载
  • 安徽百沃生物医药怎么样?中药材大型合作种植基地技术赋能农户增收 - 资讯快报
  • Unity WebGL截图下载全链路解析:从Canvas到Blob的五重关卡
  • 2026亲测:专业降AI率网站TOP1推荐
  • 临床试验缺失数据处理:多重插补方法对比与机器学习应用指南
  • AI时代科技巨头重返PC战场,PC有望重塑为下一代计算生态核心入口
  • JMeter接口与性能测试本质区别及工程化实践
  • 影刀RPA店群自动化:脚本自动修复与智能运维实践
  • 物理信息机器学习超参数选择难题:PILE分数如何提供统计最优解?
  • AIC8800DC在Kali无法启用monitor mode的根源与修复
  • 2026 全国智慧景区建设服务商综合评测:湖南途记互联稳居行业排名第一 - 资讯快报
  • 行业特色鲜明、以后不用愁就业的大学?基于多维能力的高校对比 - 资讯快报
  • 告别Unity自带播放器!用AVPro Video 2.7.3搞定安卓/PC多平台视频播放(含StreamingAssets配置)
  • 2026年杭州电商新星:哪家公司更值得信赖?
  • 为什么指数涨了,你的股票却在跌?
  • 频率覆盖至8GHz:鼎讯信通 OM系列台式频谱分析仪 重新定义台式频谱仪标准
  • 如何用3分钟掌握跨平台资源下载神器:从微信视频号到全网资源一键获取
  • 云算豹AI设计软件实战 30 天:平面设计师的工具选择之道 - 资讯快报
  • KityMinder思维导图终极指南:3步快速掌握你的创意整理利器
  • 龙虾之父开源Skill“体检”工具,5大功能优化技能资源负载
  • 2026 年外呼机器人哪家靠谱:云蝠智能平稳运行 - 17322238651
  • “知雀“ 电商 AI 客服 Agent:个人开发者从混合架构到模块化单体的架构与排期革命
  • Azure存储账户核心原理与生产级配置指南
  • Navicat无限试用终极指南:3种方法让Mac用户永久享受免费数据库管理
  • 2026免费一键去水印工具怎么选?一键去水印工具实测推荐