这个学期过半,研修网又弹出了继续教育的学时提醒。我打开课程列表,三门必修课,每门二十多节视频课,合计超过十个小时的学习时长。说实话,视频内容本身有价值,但一节课配一次弹窗、一集结束要点一次“下一节”、学完一个章节还要回目录手动翻到下一章,这套重复操作的次数多到让人怀疑平台故意在考验人的耐心。于是我把这个“XCC版”学习脚本整理出来,它做的事情很简单:打开课程页之后,自动检测视频播放状态,播完自动切下一节,全程把进度和日志显示在页面角落。
这篇内容适合两类人看。一类是和我一样被继续教育学时追着跑、想省下机械点击时间的学习者,你可以直接用现成思路部署到自己的浏览器里;另一类是刚接触浏览器自动化脚本、想了解油猴脚本怎么写的人,我会把播放器检测、按钮点击、iframe处理这些关键点都拆开讲。先说清楚,这不是什么破解工具,不碰登录接口、不伪造学时,它只是在浏览器里替你做了一个“看完自动点下一个”的乖学生动作。
1. 研修网学习脚本是怎么来的——一个拖延症末期的自我救赎
1.1 研修平台的“反人类”学习流程
很多人没真正打开过研修网,不知道这类平台的学习流程有多考验耐心。整个过程可以拆成这么几步:打开研修网首页,输入账号密码登录,进入课程列表,找到待学课程点进去,在一个章节目录里找到当前该看的视频,点播放,等视频播完,再点“下一节”,然后重复。
听起来不复杂,但真正跑起来全是细节问题。课程视频动不动就是20到40分钟一集,播放过程中只要鼠标离开页面太久,某些浏览器标签页会自动休眠;有的视频中途会弹出一个互动问题,不点掉就一直卡在当前进度;更烦的是章节目录不会自动刷新,你学完第一章,回到列表还得手动确认哪一章是“进行中”,眼睛盯久了真的会花。我把这些痛点总结成三个字:重复、等待、确认。
重复的是“下一节”这个动作,等待的是视频缓冲和播放结束的判定,确认的是当前进度到底走到哪了。这三个痛点叠加在一起,一个学期的研修任务能让人折腾两天。我写这个脚本的初衷,就是把“重复”交给代码,把“等待”做成可视化提醒,把“确认”交给本地记录。
1.2 脚本的边界:只做重复点击,不做内容破解
先说清楚这个脚本的定位。市面上确实有人把这类学习辅助工具做成全自动挂机,直接用接口请求伪造学习记录,或者用Headless浏览器在服务器上跑。那不是我这个脚本的方向,我也明确不建议那么做。
“XCC版”的设计原则是模拟用户行为,整个运行过程都发生在真实浏览器里,播放器是在真实播放视频的,学时记录是平台自己生成的,脚本做的只是在合适的时机帮你点一下按钮。这种做法的好处有两个:一是实现简单,不依赖任何后端服务,脚本文件复制到浏览器就能跑;二是风险可控,它没有违反平台的核心考核机制,只是在操作层面替代手动点击。
想通这一点之后,技术路线就很清晰了:前端脚本,直接跑在浏览器页面里,通过DOM操作完成点击、状态读取和信息展示。至于具体怎么实现,接下来展开讲。
2. 技术选型:为什么是油猴脚本,而不是Python爬虫
2.1 三种方案的横向对比
决定写脚本之前,我其实做过一轮选型。研修网页面是标准的网页端操作场景,候选方案无非这么几条路:油猴脚本、Python加Selenium或Playwright、桌面端模拟点击工具。列个表格看得更清楚。
| 方案 | 上手成本 | 环境依赖 | 跨平台 | 维护难度 | 适合场景 |
|---|---|---|---|---|---|
| 油猴脚本(Tampermonkey) | 低 | 浏览器 | 好 | 中 | 页面操作自动化,点击、填表、爬取页面数据 |
| Python + Playwright | 中高 | Python环境加浏览器驱动 | 一般 | 高 | 批量流程、复杂交互、后端定时任务 |
| 按键精灵类工具 | 低 | 桌面客户端 | 差 | 低 | 桌面软件自动化,坐标点击为主 |
Python方案我第一个排除掉了。它的优点是可以在服务器上跑,不需要真人守着浏览器,但代价是要装Python开发环境、下载浏览器驱动、处理登录态保存、应对验证码,整套下来工程量直接翻倍。而且研修网这类平台经常调整页面结构,Python脚本一旦偶发报错,排查还得依赖终端日志,不如浏览器里的实时反馈直观。
按键精灵看起来简单,实际用起来就会发现坐标点击太脆弱。页面分辨率一变、窗口一缩,坐标全偏。鼠标一晃动,模拟器就误判。更麻烦的是,这类工具动不动被杀毒软件报毒,分享给别人也不方便。
2.2 为什么最终落在油猴脚本上
油猴脚本的天然优势是运行在浏览器页面上下文里,对DOM的访问没有任何障碍。研修网的所有交互都在网页里完成,脚本注入页面后,可以像操作自己的网页一样操作它,不需要任何中间层。
“XCC版”这个名字里的XCC是版本命名,说明这个脚本不是从零开始闭门造车,而是在常见学习辅助脚本的框架基础上适配出来的。初版参考了通用自动播放脚本的做法,但针对研修网的页面结构做了大量精简和重写,加上了进度悬浮窗、状态日志、本地学习记录这些实用模块。
脚本的整体模块规划是这样的:
- 配置区:集中管理轮询间隔、自动切换开关、存储键名等参数,改配置不用翻代码
- 工具函数区:封装日志输出、本地存储、DOM查找等通用方法
- 核心逻辑区:播放器检测、播放状态判断、按钮点击、答题弹窗识别
- UI展示区:页面角落的悬浮窗,显示当前课程、章节、播放状态和最近日志
2.3 脚本的目录结构设计
给读者一个直观的模块划分,后续维护起来会轻松很多。核心函数表先列出来,后面逐一解释每个函数背后的设计思路。
- getPlayer():从当前页面或iframe中定位video播放器节点
- checkPlaying():读取播放器状态,判断是播放中、暂停还是已结束
- clickNextSection():查找并点击“下一节”按钮
- detectQuizPopup():检测答题弹窗是否出现
- logMessage():双通道日志,同时写入控制台和页面悬浮窗
- saveProgress():把已完成章节写入localStorage
- renderPanel():刷新悬浮窗UI
3. 核心功能逐个拆解——从监听器到模拟点击
3.1 播放状态检测:MutationObserver与轮询的取舍
研修网页面上,视频播放器的本质是一个HTML5的video标签,偶尔是iframe嵌套的播放器。要判断视频放完了没有,无非两种思路:一种是监听DOM结构变化,一种是用定时器轮询读取状态。
MutationObserver是浏览器提供的DOM监听接口,可以观察到节点新增、删除、属性变化。用它来监听播放器状态的好处是实时、精准,但坏处是写法复杂,而且研修网页面本身就有大量动态节点变化,监听器一不小心就会疯狂触发,还需要做防抖处理。对于我这种只想“等视频放完就点下一节”的场景,杀鸡用牛刀。
轮询方案就简单得多,设一个定时器,每3秒读一次video元素的paused和ended属性。3秒的延迟对自动切换来说完全无感知,代码也好调试。核心逻辑如下:
const CHECK_INTERVAL = 3000; function findPlayer() { // 优先看当前页面的video标签 const directVideo = document.querySelector('video'); if (directVideo) return directVideo; // 部分课程会把播放器嵌在iframe里 const iframe = document.querySelector('iframe'); if (iframe && iframe.contentDocument) { return iframe.contentDocument.querySelector('video'); } return null; } function checkStatus() { const player = findPlayer(); if (!player) { logMessage('warn', '未找到播放器,继续重试'); return; } if (player.paused) { logMessage('info', '播放器暂停,尝试继续播放'); player.play().catch(() => {}); } if (player.ended) { logMessage('info', '当前视频已播放完毕,准备切换下一节'); clickNextSection(); } } setInterval(checkStatus, CHECK_INTERVAL);这段代码是整个脚本的心脏。findPlayer函数我设计成先查当前文档再查iframe,顺序不能反,因为当前文档查到video的命中率最高。检查paused属性是为了应对播放器偶发自动暂停的情况,比如页面切后台回来之后播放器可能暂停,脚本会尝试恢复播放。检查ended属性则是触发切换的前提。
3.2 自动“下一节”与iframe嵌套处理路径
clickNextSection是整个脚本里变量最多的部分,因为不同版本的研修网页面,“下一节”按钮的实现方式完全不一样。有的版本是一个a标签,有的版本是button,还有的版本根本没有独立按钮,得从章节列表里自己定位当前章节的下一个兄弟节点。
通用写法是先按多种选择器尝试,找到按钮就点,找不到就退回列表方案:
function clickNextSection() { const selectors = [ '.next-btn', '.next-section', '.btn-next', 'a.next', '[class*="next"]' ]; for (const sel of selectors) { const btn = document.querySelector(sel); if (btn && !btn.disabled) { btn.click(); logMessage('success', '已点击下一节按钮'); return; } } // 找不到按钮,退回从章节列表里解析顺序 logMessage('warn', '未识别到下一节按钮,尝试从列表定位'); // 这里可以根据页面实际结构编写列表跳转逻辑 }这里有个细节值得提一下:点击之前一定要检查disabled属性。有些按钮在视频未播完时是灰色不可点状态,直接click不会生效,但脚本不知道,会误以为切换成功。加了disabled判断之后,如果按钮不可点,日志会准确提示“当前处在不可切换状态”,而不是静默失败。
iframe的处理是另一个容易踩坑的地方。如果播放器在iframe里且这个iframe和主页面同源,可以通过contentDocument拿到内部的video并操作;如果跨域,浏览器的同源策略会直接拒绝访问,任何脚本都无法绕过。遇到这种跨域情况,我的策略是识别到播放器不可访问时,在悬浮窗明确提示“当前课程的播放器跨域,请手动确认切换”,不让脚本假装自己还能干活。
3.3 答题弹窗:只做检测和提醒,不做代答
研修网课程视频放到一定进度时,经常弹出互动题目。弹窗出现时,视频会暂停,必须做一道选择题才能继续。很多辅助脚本会连这一步也自动化,直接读题目、匹配选项、自动点击提交。
我在写“XCC版”的时候,刻意没有给自己做全自动答题。原因有两个。第一是学习的价值在于内容本身,自动代答等于把整门课的学习过程全面架空,最后课时刷完了但脑袋空空,真正考核时吃亏的还是自己。第二是题目答案的匹配压根做不到稳定,平台题库一更新,硬编码的答案映射马上就废,维护成本无穷无尽。
所以答题模块做成了提醒模式:检测到弹窗时,脚本把题目文本和选项列表提取出来,打印到悬浮窗日志里,同时把页面滚动到弹窗区域,提醒你来处理。代码核心就是检测弹窗容器是否存在:
function detectQuizPopup() { const popup = document.querySelector('.quiz-popup, .question-box, [class*="quiz"]'); if (popup && !popup.classList.contains('xcc-reminded')) { popup.classList.add('xcc-reminded'); const question = popup.innerText.trim().slice(0, 200); logMessage('warn', '检测到答题弹窗,请手动作答:' + question); } }加一个自定义class标记的目的,是为了避免同一个弹窗反复提示刷屏。你作答完成后弹窗消失,下一次新弹窗出现时class标记不存在,又会触发提醒。这个设计在实战里特别好用,不会被日志淹没。
3.4 进度记忆与悬浮窗设计
看完一个章节之后,脚本会在localStorage里记录完成状态。这样即使你关掉了浏览器,下次打开课程页面,悬浮窗也能直接展示“进度:3/20”,而不是从零开始猜测。
存储格式我设计得尽量简单,就是一个JSON对象,键名固定:
function saveProgress(courseId, chapterId) { const key = 'xcc_learn_progress'; const data = JSON.parse(localStorage.getItem(key) || '{}'); if (!data[courseId]) data[courseId] = []; if (!data[courseId].includes(chapterId)) { data[courseId].push(chapterId); localStorage.setItem(key, JSON.stringify(data)); } }悬浮窗的实现也值得说说。一个固定定位在页面右下角的div,带一点半透明背景,实时显示三行信息:当前课程名、当前章节号、播放状态。日志则单独放一个可折叠区域,最多保留最近十条,再早的丢弃,避免DOM节点无限增长拖慢页面。
function renderPanel() { const panel = document.getElementById('xcc-panel'); if (!panel) return; panel.innerHTML = ` <div class="xcc-status">状态:${statusText}</div> <div class="xcc-progress">进度:${currentIndex}/${totalCount}</div> <div class="xcc-log">${logLines.join('<br>')}</div> `; }这个悬浮窗帮了大忙,尤其是后台挂着的时候,瞥一眼就知道脚本在正常跑还是卡住了,不用每次切回浏览器废半天劲检查。
4. 安装、配置与实测体感
4.1 环境准备与油猴插件安装
在正式使用脚本之前,需要准备一个能运行用户脚本的浏览器环境。推荐用Chrome或者Edge,先到应用商店安装Tampermonkey扩展,也就是圈子里常说的油猴。安装好之后,工具栏会出现油猴图标,点击图标选择“添加新脚本”,把脚本代码整体粘贴进去保存即可。
需要注意的一个细节是,Tampermonkey的脚本作用域默认匹配所有网址,这会导致无关页面也执行脚本,所以必须在脚本头部写清匹配规则。一般来说,研修网的课程页面URL有固定路径特征,把匹配规则精确到课程页路径下,其他页面一律不执行,避免脚本在首页和资料页乱点。这一步非常关键,不匹配好规则,脚本在其他页面触发了“下一节”逻辑,可能把浏览器开的一堆无关页面全部搞乱。
4.2 核心配置项说明
脚本开头的配置区集中管理了所有可调参数,默认值已经过我实测验证,直接跑没问题,但你可以根据自己的实际情况调整。
| 配置项 | 默认值 | 说明 |
|---|---|---|
| CHECK_INTERVAL | 3000 | 播放状态轮询的毫秒数,改小响应更快但费资源 |
| AUTO_NEXT | true | 是否自动点击下一节,设false就只检测不切换 |
| AUTO_PLAY | true | 暂停时是否自动尝试恢复播放 |
| SHOW_PANEL | true | 是否显示页面悬浮窗 |
| PANEL_POSITION | 'right-bottom' | 悬浮窗位置,支持四个角 |
| STORE_KEY | 'xcc_learn_progress' | localStorage存储键名,多人共用浏览器时可以改掉 |
CHECK_INTERVAL不建议低于1000毫秒,否则每秒钟访问一次DOM,页面会有明显卡顿感,而且频繁操作播放器还可能触发平台的风控逻辑。3000毫秒是在灵敏度和资源占用之间的平衡点,实测下来完全够用。
4.3 实测效果与真实预期管理
我拿自己手头的一门课程做了完整测试,课程共20节视频,每节平均25分钟,总时长将近8个小时。手动点的话,需要全程守在电脑前,每隔25分钟回来点一次“下一节”,中途还要处理几次互动弹窗,少说得占用大半天的时间。
用“XCC版”跑同一门课,实际操作量降到了三次:第一次是开头登录并进入课程页,第二次是中途回来答掉一个互动题,第三次是课程结束时确认一下总进度。视频在播放期间脚本不会干预,播放器真实地流式加载视频,到了结尾自动执行切换动作。整个流程下来,悬浮窗里的日志清晰记录了每一步操作,我心里有数它到底干了什么。
但我得给一个真实的预期管理:这类脚本没法做到“挂着睡一觉起来全部学完”。登录失效、网络断线、平台临时调整页面结构、验证码弹窗,这些都是脚本无法绕过的不确定因素。你不能向它承诺百分百无人值守,但能承诺把你99%的重复点击从每天里删掉。每次跑完一门课,回头看看悬浮窗里那十几条偶尔需要人工确认的日志,就会明白哪些环节值得被自动化,哪些环节还是得自己来。
5. 平台升级也是最坑的升级——版本维护实录
5.1 选择器失效的完整排查链路
脚本写完之后,最大的敌人不是需求变化,而是平台改版。研修网这种平台每隔一段时间就会调整前端代码,改版之后最常见的现象就是脚本完全失灵,悬浮窗不更新,自动切换不触发,看似一切正常但实际一动不动。
一次典型的失效排查大概长这样。打开课程的任意章节页,按F12打开开发者工具,切到Elements面板。用鼠标点一下页面上那个“下一节”按钮,右侧会高亮出对应的DOM节点。仔细看这个节点的class和id,对比脚本里用的那个选择器。如果class变了,比如原来叫next-button现在改成了btn-next-step,脚本里的document.querySelector('.next-button')就会返回null,一切逻辑自然停摆。
定位到问题之后,把脚本里的选择器更新成新class名,保存脚本,刷新课程页面,看到悬浮窗里的日志开始正常输出“已点击下一节按钮”,故障就算解决了。整个过程熟练的话五分钟内搞定,这也是为什么我坚持把选择器统一放到一个函数里而不是散落在代码各处,改起来只需要动一处。
5.2 选择器匹配的健壮性设计
经历过一次改版之后,我学乖了。现在脚本里所有选择器的写法都带有多路径回退。比如查找下一节按钮时,会准备四五个可能的选择器,从上到下依次尝试,只要命中一个就不继续往下找。
这就是我在前面clickNextSection里用选择器数组的原因,不是什么花哨技巧,就是被平台改版打磨出来的防御式写法。尽量使用HTML结构特征而不是class名称作为锚点,class名是前端最常改的属性,而页面层级结构除非整站重构,否则相对稳定一些。
5.3 登录态、多标签互顶与网络波动
维护过程中遇到的问题,除了选择器失效,还有三类比较常见。
登录态过期是最高频的。脚本运行时间长了,cookie过期,页面自动跳转到登录页,此时脚本找不到播放器也找不到按钮,悬浮窗只会持续输出“未找到播放器,继续重试”。这种情况下脚本无法自动重新登录,因为我刻意不把账号密码写进脚本里,任何自动化工具只要开始存储账号密码,风险等级就完全不一样了。用户在悬浮窗看到持续警告时,手动登录一次就好,登录后刷新课程页脚本会继续工作。
多标签页同时打开课程,也会出问题。很多平台限制了同一账号的在线会话数,一个标签页登录后,另一个标签页会被顶掉。表现在脚本上就是明明看着页面正常,但视频永远加载不出来。我的规避策略是在脚本里加上单实例保护,检测到当前页面不是唯一打开的同域名课程页时,在悬浮窗提示关闭其他标签页,而不是闷头干活。
网络波动的问题更隐蔽。视频播放到99%时突然断网,播放器不会触发ended事件,脚本会一直在等待状态。所以我在轮询逻辑里加了超时保护:如果连续10次检查(约30秒)都检测到播放器存在但进度没有前进,就输出一条日志提醒用户检查网络。用进度值变化来判断比单纯看paused和ended更可靠,因为很多播放器在缓冲时并不处于paused状态。
6. 最后说点真心话:脚本边界与个人体会
这个脚本从第一次跑通到现在,中间迭代了很多版本,每次改版都踩在平台更新的痛点上。回头看,最有价值的不是那几行点击逻辑,而是整个迭代过程里积累的浏览器自动化经验。
我自己现在的使用习惯,是把重点课程当成正常网课看,专心盯着内容、记笔记;遇到那种明显是形式主义的重复性课程,才让脚本挂机跑进度。脚本的定位始终是“重复操作替代器”,不应该是“学习替代器”。
如果你拿着这个脚本,想改造成全自动答题,我的建议是慎重。不是技术上行不通,而是当一门课从头到尾都不用你动脑的时候,你其实在用最宝贵的时间换一个毫无意义的完成状态。偶尔偷懒无可厚非,完全放弃思考就本末倒置了。
最后分享一个我实际用下来的小技巧:每次跑完一门课,把脚本悬浮窗里的学习日志截图保存下来,连同自己对这门课的笔记一起归档。等到研修考核需要提交学习总结时,翻一翻这些记录,写总结的速度比从零开始回忆快得多。脚本帮你省下的时间,最好还是花在能让你自己真正成长的地方。