资讯动态

浏览器自动化实战:油猴脚本与Playwright技术选型及核心实现

发布时间:2026/10/9 18:48:49 来源:尧图企业网站定制
1. 从需求到方案自动化操作的技术选型思路1.1 这类需求到底在解决什么问题先把场景说清楚。很多在线学习平台会记录视频观看进度、章节完成状态、习题提交情况而学员端往往需要反复点击、等待、切换页面。当课程数量多、章节碎、时间窗口紧的时候纯手工操作就变成了一件极其消耗精力的事。于是就有了“用脚本代替重复点击”的需求。这个需求本质上属于浏览器端自动化操作它要解决的核心问题有三个第一识别页面上的播放按钮、进度条、下一节按钮等元素第二模拟真实的用户行为点击、滚动、等待第三在长时间运行中保持稳定不因为页面刷新、弹窗、网络抖动而中断。我见过不少人一上来就想着“写个脚本一把梭”结果要么被前端反自动化机制拦住要么跑十分钟就卡死。所以方案选型这件事值得先花时间想清楚而不是直接开写。1.2 两条主流技术路线油猴脚本 vs Playwright目前社区里讨论最多的两条路线一条是油猴脚本UserScript一条是Playwright。它们不是替代关系而是适用于不同阶段的两种工具。油猴脚本的本质是一段注入到目标页面里执行的 JavaScript。它运行在浏览器已有的页面上下文里直接操作 DOM天然带着登录态、Cookie、localStorage不需要额外处理会话。优点是轻、快、零部署装个浏览器扩展就能跑。缺点是它受限于页面本身一旦页面做了反自动化检测比如检测document.hidden、监听鼠标轨迹、校验事件isTrusted油猴脚本很容易被识别。Playwright 则是浏览器自动化框架它通过 CDPChrome DevTools Protocol驱动一个真实的浏览器实例从外部控制页面。它不依赖页面注入能模拟真实的鼠标移动、键盘输入、触摸事件还能拦截网络请求、修改响应。缺点是重需要 Node.js 环境、需要安装浏览器内核、需要自己维护登录态。我的建议很直接如果只是自己用、页面没有强反自动化、追求快速见效油猴脚本足够如果要长期稳定运行、要处理复杂交互、要做批量任务调度直接上 Playwright别在油猴上反复折腾。1.3 为什么最终倾向 Playwright说几个我实际踩过的点。油猴脚本在页面切换时会被卸载重载长任务容易断页面一旦更新 DOM 结构选择器就失效遇到 iframe 嵌套的播放器跨域限制直接让脚本抓瞎。而 Playwright 的frameLocator能穿透 iframewaitForSelector能等元素稳定出现context.storageState()能把登录态持久化下来复用这些都是油猴脚本很难优雅做到的。另外 Playwright 支持无头模式和有头模式切换。调试阶段用有头模式肉眼看着它操作出问题一眼就能定位跑批量任务时切无头资源占用低。这种灵活性对实际项目太重要了。提示技术选型没有绝对的对错只有匹配度。先评估目标页面的复杂度、反自动化强度、运行时长再决定用哪条路线比盲目跟风强得多。2. 油猴脚本路线轻量方案的实现细节2.1 油猴脚本的基本结构与注入时机油猴脚本的头部是一段元数据注释用来声明脚本名称、匹配的网址、运行时机等。运行时机这个参数特别关键常见的有document-start、document-end、document-idle。选错了脚本要么在 DOM 还没建好时就执行导致找不到元素要么等太久错过关键时机。// UserScript // name 学习进度辅助 // namespace local.helper // version 1.0 // description 辅助完成页面内的重复点击操作 // match https://example-learning-site.com/* // grant none // run-at document-idle // /UserScriptgrant none表示脚本不调用油猴提供的特殊 API直接在当前页面上下文运行这样能直接访问页面的全局变量。如果需要跨域请求或者访问油猴的存储 API就要改成grant GM_xmlhttpRequest之类但那样脚本会运行在沙箱里访问页面变量会受限。这个取舍要根据实际需求定。2.2 元素定位与点击的稳定性处理页面元素定位是油猴脚本最容易翻车的地方。很多人习惯用document.querySelector(.play-btn)但类名一旦被前端框架动态生成比如带哈希后缀选择器立刻就失效。更稳的做法是结合多种特征定位比如文本内容、层级关系、属性组合。function findPlayButton() { // 优先用稳定的语义属性 let btn document.querySelector([data-roleplay]); if (btn) return btn; // 退而求其次用文本内容匹配 const candidates [...document.querySelectorAll(button, div[rolebutton])]; return candidates.find(el /播放|继续|开始/.test(el.textContent)) || null; }点击也不能直接el.click()了事。有些页面监听的是mousedown/mouseup组合有些校验事件的isTrusted属性。用dispatchEvent手动构造事件时isTrusted永远是false容易被识别。所以能触发原生click()就优先用它实在不行再考虑模拟完整的事件序列。2.3 定时轮询与状态判断视频播放类页面通常需要等待播放结束再点下一节。这里不能用固定setTimeout因为视频长度不一、网络加载有快慢。正确做法是轮询状态每隔一段时间检查一次进度条或播放状态。function waitUntilFinished(checkFn, interval 3000, timeout 3600000) { return new Promise((resolve, reject) { const start Date.now(); const timer setInterval(() { if (checkFn()) { clearInterval(timer); resolve(); } else if (Date.now() - start timeout) { clearInterval(timer); reject(new Error(等待超时)); } }, interval); }); }轮询间隔别设太短1 秒以内会给页面带来不必要的负担也可能触发性能监控告警3 到 5 秒是比较舒服的区间。超时时间一定要设否则脚本可能永远卡在那里。2.4 油猴脚本的天然局限用了这么久油猴脚本的短板我总结成三条。第一无法脱离浏览器标签页页面一关脚本就停第二难以处理多标签协同比如同时开多个课程页面脚本之间互相不知道对方状态第三反自动化检测难绕过页面只要检查一下navigator.webdriver、鼠标移动轨迹、事件时间间隔就能把脚本行为识别出来。所以油猴脚本适合“短平快”的场景一旦需求升级到批量、长时、复杂交互就该考虑换工具了。3. Playwright 实战从环境搭建到核心逻辑3.1 Node.js 环境准备与版本选择Playwright 基于 Node.js所以第一步是把运行环境搭好。Node.js 的版本建议选LTS 长期支持版比如 18 或 20 系列。太新的版本可能和某些依赖不兼容太旧的版本又缺少新特性。在 Linux 环境下安装推荐用 NodeSource 提供的源比系统自带的包管理器版本新很多# 添加 NodeSource 源以 20.x 为例 curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt-get install -y nodejs # 验证 node -v npm -vWindows 环境直接去官网下安装包一路下一步即可。安装完记得把 npm 的全局目录加到 PATH 里否则全局安装的命令行工具会找不到。注意如果公司网络有代理限制npm 安装依赖时可能需要配置 registry。这个属于环境问题遇到报错先检查网络连通性别急着怀疑代码。3.2 Playwright 安装与浏览器内核管理装好 Node.js 后初始化项目并安装 Playwrightmkdir auto-task cd auto-task npm init -y npm install -D playwright/test npx playwright install chromium这里有个坑要提前说npx playwright install会下载浏览器内核体积不小网络不好的时候容易失败。可以指定只装需要的浏览器比如只用 Chromium 就npx playwright install chromium别一股脑全装。另一个常见报错是chrome-headless-shell.exe doesnt exist这类提示通常是因为内核没装全或者版本不匹配。解决办法是删掉缓存目录重新装# 清理缓存后重装 npx playwright install --force chromium3.3 启动浏览器与登录态持久化Playwright 的核心对象是Browser、BrowserContext、Page。BrowserContext相当于一个独立的浏览器会话Cookie、localStorage 都隔离在 context 里。把登录态持久化下来后续任务就不用反复登录。const { chromium } require(playwright); (async () { const context await chromium.launchPersistentContext(./user-data, { headless: false, viewport: { width: 1280, height: 800 }, args: [--disable-blink-featuresAutomationControlled] }); const page await context.newPage(); await page.goto(https://example-learning-site.com/login); // 手动登录一次登录态会保存在 ./user-data 目录 await page.waitForTimeout(60000); await context.close(); })();launchPersistentContext会把用户数据目录持久化到磁盘下次启动直接带着登录态。第一次跑用有头模式手动登录之后就可以切无头模式批量跑了。--disable-blink-featuresAutomationControlled这个参数能去掉一部分自动化特征降低被识别的概率。3.4 页面元素定位的几种策略对比Playwright 的定位器比原生 DOM 查询强太多它内置了自动等待机制元素不可见、不可点击时会自动重试。常用的定位方式有这么几种定位方式写法示例适用场景稳定性角色定位getByRole(button, { name: 播放 })语义化良好的页面高文本定位getByText(继续学习)按钮文字固定中测试 IDgetByTestId(play-btn)有专门测试属性的页面最高CSS 选择器locator(.video-item .play)结构稳定的页面中XPathlocator(//div[classplay])复杂层级关系低我的经验是优先用角色定位和测试 ID这两类最稳。CSS 选择器作为兜底XPath 尽量别用页面稍微一改就废。3.5 处理 iframe 嵌套的播放器在线学习平台的视频播放器经常嵌在 iframe 里这是油猴脚本的噩梦但 Playwright 处理起来很从容const frame page.frameLocator(iframe[src*player]); await frame.getByRole(button, { name: 播放 }).click();frameLocator会自动穿透 iframe后续的定位都在 frame 内部进行。如果 iframe 是动态加载的配合waitFor等待它出现await page.waitForSelector(iframe[src*player], { state: attached });3.6 模拟真实用户行为的技巧反自动化检测里鼠标轨迹和事件时间间隔是两个重点。Playwright 的mouse.move支持分步移动可以模拟出带加速度的轨迹await page.mouse.move(100, 100); await page.mouse.move(300, 200, { steps: 25 }); await page.mouse.click(300, 200);steps参数让鼠标分 25 步移动过去而不是瞬移这样轨迹看起来更自然。点击之间的间隔也别太规律加一点随机延迟const randomDelay (min, max) new Promise(r setTimeout(r, min Math.random() * (max - min))); await randomDelay(800, 2000);这种随机化处理能显著降低被行为分析识别的概率。别小看这几百毫秒的差异机器行为最明显的特征就是“太规律”。4. 核心逻辑实现进度检测与任务调度4.1 视频进度检测的三种思路判断视频是否播放完有三种常见思路。第一种是读取进度条元素的宽度或属性比如aria-valuenow第二种是监听视频元素的ended事件第三种是轮询播放时间与总时长。// 思路三轮询 video 元素的 currentTime async function waitVideoEnd(page, timeout 3600000) { const start Date.now(); while (Date.now() - start timeout) { const ended await page.evaluate(() { const v document.querySelector(video); if (!v) return false; return v.ended || (v.duration 0 v.currentTime v.duration - 1); }); if (ended) return true; await page.waitForTimeout(3000); } throw new Error(视频等待超时); }page.evaluate能在页面上下文里执行代码直接访问video元素。注意duration在视频元数据加载前是NaN所以要判断v.duration 0。4.2 章节切换与状态记录一个课程通常有多个章节需要按顺序完成。这里要维护一个已完成章节的集合避免重复操作也方便断点续跑。const completed new Set(); async function processChapters(page) { const chapters await page.locator(.chapter-item).all(); for (let i 0; i chapters.length; i) { const key chapter-${i}; if (completed.has(key)) continue; await chapters[i].click(); await page.waitForLoadState(networkidle); await waitVideoEnd(page); completed.add(key); console.log(章节 ${i 1} 完成); } }completed集合可以持久化到本地文件这样脚本中断后重启能接着跑不用从头再来。4.3 异常处理与自动重试长任务最怕的就是中途出异常。网络抖动、元素没加载出来、弹窗遮挡任何一个都能让脚本停摆。所以每个关键操作都要包一层重试逻辑async function retry(fn, times 3, delay 2000) { for (let i 0; i times; i) { try { return await fn(); } catch (err) { console.warn(第 ${i 1} 次尝试失败: ${err.message}); if (i times - 1) throw err; await new Promise(r setTimeout(r, delay)); } } } // 使用 await retry(() page.getByRole(button, { name: 播放 }).click());重试次数别设太多3 次足够。超过 3 次还失败说明是结构性问题继续重试只是浪费时间应该记录日志人工介入。4.4 多任务并发与资源控制如果要同时处理多个课程可以用多个BrowserContext并发。但并发数不能无限加每个 context 都占内存和 CPU开太多反而拖慢整体速度。const CONCURRENCY 3; const tasks [...]; // 任务列表 async function runWithLimit(tasks, limit) { const executing []; for (const task of tasks) { const p task().then(() executing.splice(executing.indexOf(p), 1)); executing.push(p); if (executing.length limit) await Promise.race(executing); } await Promise.all(executing); }并发数建议从 2 到 3 开始试观察 CPU 和内存占用再调整。盲目开到 10 个机器直接卡死得不偿失。5. 常见问题排查与避坑经验5.1 元素定位失败的排查顺序元素找不到是最常见的问题排查要按顺序来。先确认页面是否真的加载完了用page.waitForLoadState(networkidle)等网络空闲再确认元素是否在 iframe 里用page.frames()打印所有 frame 的 URL然后确认选择器是否写对用page.locator(selector).count()看匹配到几个最后确认元素是否可见isVisible()返回 false 说明被隐藏了。现象可能原因解决方向定位超时元素未加载加 waitForSelector匹配到 0 个选择器错误用 DevTools 重新确认匹配到多个选择器太宽泛加父级限定元素不可点击被遮挡或禁用滚动到可见或等待启用iframe 内找不到未切换 frame用 frameLocator5.2 反自动化检测的应对思路页面识别自动化行为通常看几个信号navigator.webdriver为 true、鼠标移动是瞬移、事件间隔过于规律、无头模式的特征。应对方式前面提过一些这里补充几点。启动参数里加--disable-blink-featuresAutomationControlled能去掉一部分特征用addInitScript在页面加载前注入脚本覆盖掉一些检测属性await context.addInitScript(() { Object.defineProperty(navigator, webdriver, { get: () undefined }); });但要说清楚这些手段只是降低被识别的概率不是万能的。页面如果上了专业的风控系统行为分析维度非常多单靠几个参数覆盖是挡不住的。所以别把精力全花在对抗检测上把核心逻辑做稳才是根本。5.3 无头模式下的常见坑无头模式跑得快、省资源但有些页面在无头模式下表现不一样。比如某些视频播放器在无头模式不自动播放某些字体渲染导致元素尺寸变化。遇到这种情况可以试试新版无头模式const browser await chromium.launch({ headless: true, channel: chromium // 使用新版无头 });新版无头模式Chromium 的--headlessnew更接近真实浏览器行为兼容性问题少很多。如果还是不行就老老实实用有头模式配合虚拟显示Linux 下的 Xvfb跑在服务器上。5.4 长时间运行的稳定性保障跑几个小时的脚本稳定性是头等大事。我的经验是做好三件事定期截图留证出问题时能回看现场日志分级输出info 记录进度、warn 记录异常、error 记录致命错误心跳检测主循环每隔一段时间打印一次状态方便判断脚本是否卡死。setInterval(() { console.log([心跳] ${new Date().toISOString()} 当前进度: ${currentIndex}); }, 60000);另外内存泄漏也要防。长时间运行后page对象可能积累大量缓存定期重建 context 能缓解这个问题。跑几十个任务后重启一次浏览器比一直跑到底更稳。5.5 合规使用的边界意识最后必须说清楚一件事自动化工具的使用要遵守平台的服务条款和相关规定。很多学习平台的用户协议里明确禁止使用脚本刷课违反可能导致账号受限。技术本身是中性的但用在什么地方、怎么用需要自己判断清楚。我分享这些技术细节目的是让大家理解浏览器自动化的原理和实现方式这些知识在合法的自动化测试、数据采集、办公提效等场景里同样适用而且价值更大。把 Playwright 用在自动化测试上帮团队回归验证功能用在数据整理上把重复的表格操作自动化用在个人效率工具上减少机械劳动。这些才是这类技术真正该发光的地方。6. 从脚本到工程可维护性的几点思考6.1 配置与代码分离一开始写脚本很多人把网址、选择器、等待时间全硬编码在代码里。页面一改就得翻遍代码找地方改。更好的做法是把这些抽到配置文件// config.js module.exports { baseUrl: https://example-learning-site.com, selectors: { playButton: [data-roleplay], chapterItem: .chapter-item, video: video }, timing: { pollInterval: 3000, retryTimes: 3 } };代码里引用配置改起来只动一个文件。这个习惯在项目变大后能省大量时间。6.2 日志与可观测性脚本跑起来之后你得知道它在干什么。光靠console.log不够建议用带时间戳和级别的日志库输出到文件。这样出问题能回溯也能统计每个环节的耗时找出性能瓶颈。function log(level, msg) { const line [${new Date().toISOString()}] [${level}] ${msg}; console.log(line); fs.appendFileSync(run.log, line \n); }日志别写太啰嗦关键节点记录即可。每个章节开始、结束、异常各记一条足够定位问题。6.3 断点续跑的设计长任务最怕跑到一半断了要从头来。把进度持久化到文件启动时先读进度跳过已完成的部分const progressFile ./progress.json; let progress fs.existsSync(progressFile) ? JSON.parse(fs.readFileSync(progressFile)) : { completed: [] }; function markDone(id) { progress.completed.push(id); fs.writeFileSync(progressFile, JSON.stringify(progress)); }这个设计看起来简单但实际用起来能救命。尤其是跑几十个章节的任务断一次重来成本太高。6.4 把脚本封装成可复用模块如果同类任务会反复做别每次都复制粘贴。把通用逻辑抽成模块比如“等待视频结束”“重试点击”“持久化进度”下次换个页面只改配置就行。这样积累下来你会有一套自己的自动化工具箱效率提升是复利式的。我在实际项目里的体会是写脚本的时间永远比维护脚本的时间短。一开始多花点心思在结构上后面改需求、换页面的时候会轻松很多。那些图快一把梭的代码往往第二周就得推倒重来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑