
如果你在读这篇文章之前只是听说过“前端自动化可以刷网课进度”但一直没搞明白里面到底怎么回事那这篇应该能帮上忙。我这里直接用 JavaScript 展开围绕“在线课群课程视频立刻完成”这件事讲了它不是无脑改个数字而是有一套完整的原理和落地路径。先说清楚面向的人群想搞懂播放器进度判定原理的开发者、需要批量处理自己测试课程的学习记录的人以及单纯对浏览器自动化感兴趣的朋友。本文讲的不是“鼓励你逃课刷学分”而是从工程实现角度拆解这套自动完成方案并会明确告知哪些做法容易被平台反制。1. 这方案到底在解决什么问题1.1 课群视频的完成机制是什么样的“课群视频”这类形态常见于高校在线教学平台的课群功能里。只要加入某个课群系统就会给你分配一组视频课程通常要求你逐条点开、播放、看到结尾然后界面上的“学习进度”才会从 80%、90% 刷新到 100%。平台判断你是不是“看完了”依靠的并不是摄像头盯着你的脸而是浏览器里的一系列事件上报。简单来说视频页面上有一个 HTML5 视频播放器播放器在运行过程中会不断产生播放状态、播放位置、播放时长等数据前端把这些数据打包发给后端后端再据此更新你的学习记录。所以本质上你所谓的“完成”在平台眼里就是一组数据某个用户的 ID、某个课程视频的 ID、当前进度到了哪一帧、累计播放了多少秒。只要这组数据满足平台预设的阈值系统就认定学习记录成立。1.2 为什么说“立刻完成”在技术上可行这里的关键在于大多数平台的“完成判定”是可以在前端被模拟出来的而不需要在后端做任何注入或攻击。视频播放器底层有一个video元素这个元素暴露了currentTime和duration两个核心属性。前者表示当前播放到第几秒后者表示视频总时长。当currentTime被设置到接近或等于duration时播放器就会认为视频已经播放完毕进而触发ended事件。前端拿到ended事件后会去执行“上报完成”这个动作。也就是说如果你的目标是让页面显示“已学完”只需要操纵currentTime和事件流让播放器和平台的前端逻辑都认为你已经看完。这一步可以用一段 JavaScript 完成甚至不需要借助任何外部工具打开开发者工具的控制台就能跑。但请不要高兴得太早。“可行”不等于“一定成功”。后面章节会解释为什么有些平台能防住这种做法以及真正稳妥的自动化应该怎么写。1.3 这个方案适合谁不适合谁适合的人想测试自己写的自动化脚本、需要在自己负责的测试环境里验证视频播放完成逻辑、或者只是纯粹对前端安全和反作弊感兴趣的开发者。不适合的人想靠它刷真实课程学分、想绕过老师布置的学习任务、想在关键课程上走捷径的人。原因很简单一部分平台会在服务端记录真实的播放时长。如果一个 40 分钟的视频你 3 秒完成服务端稍微做一次数据校验就能看出来异常轻则学习记录作废重则账号被风控标记。本文所有代码只用于学习和技术验证别搬到正式课程上给自己添麻烦。2. 核心原理播放器状态与进度上报2.1 播放器状态从哪来先说浏览器里最基础的事实只要是网页播放视频绝大多数平台都逃不开 HTML5 标准的video标签。即使你看到的是定制皮肤播放器它底层仍然包裹着一个或多个video元素只是外观被 CSS 重新绘制了而已。这意味着你可以随时在控制台里找到它、读取它、甚至直接修改它。先看一眼播放器的基本状态const video document.querySelector(video); if (video) { console.log({ duration: video.duration, currentTime: video.currentTime, paused: video.paused, ended: video.ended, readyState: video.readyState }); }这段代码做的事情很简单查有没有video标签然后打印它的时长、当前播放位置、是否暂停、是否结束、加载状态。readyState是一个 0 到 4 的数字代表视频加载到了哪一个阶段。4 表示已经有足够数据可以播放。很多新手脚本直接改currentTime却失败了原因就是视频元数据还没加载完找不到duration赋值进去根本没意义。后面我会给出处理这个问题的写法。2.2 进度上报的三种常见方式平台拿到“你看到哪一秒”的方式不是唯一的我拆成三类上报方式典型触发时机自动完成的难易程度定时轮询每隔几秒或十几秒前端主动向后端上报一次当前进度比较容易只要播放器状态正常事件驱动监听timeupdate、ended等播放事件在事件回调里上报需要触发对应事件难度中等结束一次性上报视频播放结束后页面自动或点击按钮后上报最终结果只要能触发“结束”状态就能完成定时轮询的场景你只要保证页面里的播放器没被暂停并且能持续产生“播放中”的假象平台就会正常收到进度。事件驱动的场景重点是要人为触发足够多且合理的事件流让前端误以为视频真的播完了。结束一次性上报最好处理因为你只要让播放器进入“结束”状态剩下的交给你模拟出来的一整套回调去执行。2.3 后端到底校验什么很多人以为改个前端状态就算完实际上后端往往会做以下几类校验completed字段是否出现。这是最基础的校验前端上报“完成”后端就更新记录。观看时长是否合理。一个 30 分钟的课程如果你只用了 10 秒后端日志里会有明显异常。进度连续性是否成立。比如你上一次上报在第 269 秒下一次直接报第 1800 秒中间没有任何过渡记录这就不像是真人观看。会话与身份是否一致。包括 Cookie、用户代理、IP 地址是否在同一个学习会话内保持稳定。这也解释了为什么网上那些“一行代码秒完成视频”的教程很多人用了之后进度根本没有变化。因为平台不是只看前端的最终状态而是要确认前端传上来的“完成”是可信的。所谓可信就是数据要像一个人真的打开视频、连续观看到末尾那样。2.4 看懂网络请求才是破局点如果你只想针对某个具体平台写自动化最有价值的动作不是猜它怎么实现而是打开开发者工具的 Network 面板然后手动播放一小段视频观察页面发出了哪些请求。筛选XHR或Fetch一般能看到类似progress、report、watch、study-record之类的接口名。点开某个请求看看它的Payload字段里带了什么参数比如{ courseId: 1001, videoId: 887, currentTime: 12.4, duration: 900, status: 0 }看到这种接口后判断就变得非常清晰前端负责把currentTime推上去后端负责存状态。你的自动化脚本要做的无非是让currentTime能变成duration对应的值并且在合适的时机把status变成完成状态。别小看这一点绝大多数学员的自动化脚本失败就是因为根本没去看接口长什么样只知道用currentTime duration结果等于闭着眼睛修改一个不存在的目标。3. 实操落地一个可运行的基础脚本3.1 先摸清页面结构动手写脚本之前建议先花两分钟做三件事打开视频页面按 F12 进入开发者工具。在 Elements 面板里搜索video标签确认其出现位置和父子结构。在控制台里输入document.querySelectorAll(video).length看看页面上到底有几个播放器。有的平台页面里有隐藏的video但真正的播放器是另一个找到那个唯一有可见尺寸的即可。有些页面把播放器放在了 iframe 里这种情况下主页面直接执行document.querySelector(video)会找不到。你需要先从 iframe 的文档里取const iframe document.querySelector(iframe); const doc iframe.contentDocument; const video doc doc.querySelector(video);需要注意的是如果 iframe 是跨域的contentDocument会抛异常。这时候控制台方案失效必须借助浏览器扩展脚本比如用户脚本管理器在对应 iframe 上下文中注入代码。3.2 用 JavaScript 直接把进度拉满假定你已经找到了视频元素最基础的“模拟看完”代码如下(function () { const video document.querySelector(video); if (!video) { console.error(未找到 video 元素); return; } const forceFinish () { video.currentTime video.duration; video.dispatchEvent(new Event(timeupdate, { bubbles: true })); video.dispatchEvent(new Event(ended, { bubbles: true })); video.dispatchEvent(new Event(pause, { bubbles: true })); console.log(已模拟触发播放结束当前时间, video.currentTime); }; if (video.readyState 1) { forceFinish(); } else { video.addEventListener(loadedmetadata, forceFinish, { once: true }); } })();这里有几个细节值得解释一下。为什么先判断readyState 1因为readyState为 1 表示metadata已经加载此时duration可用。如果不做这个判断在视频还没加载出时就把currentTime赋值为NaN后面什么都不会发生。为什么要手动派发timeupdate、ended、pause三个事件因为平台的前端逻辑可能分别监听不同的事件。有的靠ended触发上报有的靠timeupdate判断当前进度还有的会监听pause认为播放已结束。一次性都派发出去能最大化覆盖平台的监听器。为什么bubbles: true事件冒泡可以让父级元素也收到这个事件很多框架会把监听器绑定在播放器容器而不是video本身。不开启冒泡事件就传不到外层按钮状态不会刷新。这段代码跑完后可以到页面上看看进度百分有没有变成 100%。如果变了说明平台的核心判定就在前端如果没变说明后端做了进一步校验请看下一节。3.3 自动点击“完成学习”按钮有些平台的设计思路是视频播完后页面会出现一个“完成学习”或“确定”按钮必须手动点一下才会把记录提交到后端。这种场景下单纯触发播放器事件不够还得顺手把按钮点掉。写一个通用函数专门负责找按钮并点击function tryFinishButton() { const selectors [ [class*finish], [class*complete], [class*done], button, a ]; for (const selector of selectors) { const elements document.querySelectorAll(selector); for (const el of elements) { const text el.textContent || ; if (/完成|继续|下一节|我知道了|确认/.test(text)) { el.click(); return true; } } } return false; }这个函数的思路是从具体的选择器往宽泛的选择器去试。实际项目中按钮的 class 可能千奇百怪与其硬记选择器不如同时判断文本内容是否包含“完成”“继续”这类关键词命中率更高。但普通查询只能找到当前已经出现在 DOM 里的元素。如果是视频播放到结尾后才新弹出的按钮你需要监听 DOM 变化。这时就用到了MutationObserverconst observer new MutationObserver(() { if (tryFinishButton()) { observer.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true });这段代码会持续观察页面的 DOM 结构变化。一旦发现新出现的节点里有“完成学习”之类的元素就立刻点击然后断开观察。这样整个流程可以自动跑完不需要你盯着页面等按钮出现。3.4 用用户脚本管理器做成持久脚本控制台里的代码有个痛点刷新页面就没了。如果你需要反复测试或者需要每次进入课程页都自动执行那就要考虑把代码固化成浏览器用户脚本。先说明这里讲的是通用的浏览器用户脚本不涉及任何平台破解。我把代码放到本地个人测试环境中使用很合理// UserScript // name 课群视频自动完成本地测试环境 // namespace local.test // match http://localhost/* // run-at document-idle // grant none // /UserScript (function () { use strict; const video document.querySelector(video); if (!video) return; const forceFinish () { video.currentTime video.duration; video.dispatchEvent(new Event(timeupdate, { bubbles: true })); video.dispatchEvent(new Event(ended, { bubbles: true })); tryFinishButton(); }; video.addEventListener(loadedmetadata, forceFinish, { once: true }); function tryFinishButton() { const candidates document.querySelectorAll(button, a); for (const el of candidates) { if (/完成|继续|下一节|我知道了/.test(el.textContent || )) { el.click(); return; } } } })();注意match里的http://localhost/*这只是个示例。你可以改成https://你的测试域名/*但别轻易对正式课程域名全局生效容易造成不可控的后果。用户脚本的好处是可以在页面加载完成后自动执行不需要每次手动开控制台。3.5 为什么暴力跳转会被平台回退很多人试完上面代码发现进度确实瞬间变成 100%但刷新页面又回到原来的位置甚至被重置成 0。这其实是平台做了回退保护。回退保护常见做法是前端虽然接受了你上报的“完成”但服务端在收到请求后会二次验证该用户的实际观看日志。如果你没有足够多的观看时长记录服务端就把这次完成标记为异常并在下一次拉取进度时返回真实记录。另一种情况是播放器自己监听timeupdate事件。你强行把currentTime跳到末尾后播放器内部维护的“上次正常播放位置”还是原来的值它会回调把进度拉回合理区间。这就解释了为什么有些平台你改完没几秒播放器进度条又跳回去了。所以纯粹的“立刻完成”适合那些只做前端展示、不做后端审计的轻量级平台。稍微正规一点的平台都会在服务端至少记录一下“学习时长”或“上报间隙”。要绕过这种校验就不是改一个值这么简单而是要让整个事件流看起来“像真的”。4. 更稳妥的自动化思路模拟观看而不是虚假秒完4.1 模拟观看的核心思想如果你的目标不是“一秒完成”而是“在不长时间占用自己精力的情况下让学习记录尽可能合理”那更好的思路是模拟真实观看过程。具体来说平台希望看到的时间线是这样的视频从第 0 秒开始播放。每隔几秒上报一次位置。播放位置持续增加直到接近视频末尾。触发ended事件上报完成。模拟观看要做的就是按这个节奏去更新currentTime并且每更新一次就触发一次timeupdate。这样平台接收到的数据就有连续性和时间梯度不容易触发时长校验。下面是一个最小实现const video document.querySelector(video); if (!video) return; video.play().then(() { video.currentTime 0; const interval setInterval(() { if (video.ended) { clearInterval(interval); console.log(视频已结束); return; } // 每次播放前进 5 秒 const nextTime Math.min(video.currentTime 5, video.duration - 0.1); video.currentTime nextTime; // 手动触发时间更新事件 video.dispatchEvent(new Event(timeupdate, { bubbles: true })); }, 200); });这段代码每 200 毫秒执行一次每次把播放位置前进 5 秒。也就是说一个 60 秒的视频大概 3 秒内就跑完。从平台看到的数据上会存在多条“播放位置逐渐增加”的记录比一次性跳到末尾可信得多。当然如果你希望更接近真实可以把 200 毫秒改成 1000 毫秒5 秒改成 1 秒这样播放速度就和真实时间一致。缺点是耗时太长优点是服务端几乎没有任何理由判定异常。4.2 关于倍速播放的正确做法有经验的开发者通常会优先考虑倍速播放而不是改currentTime。倍速播放的思路是视频确实在播放平台也确实收到时间上报只是播放速度被拉快了 N 倍。HTML5 视频原生支持设置播放速度const video document.querySelector(video); video.playbackRate 8; // 8 倍速有些平台播放器封装的 UI 只允许设置 0.5、1、1.25、1.5、2 倍速你用video.playbackRate直接赋一个超过 2 的值界面上的按钮不一定显示出来但底层播放器是会按其生效的。倍速播放比跳转更稳因为它保留了播放事件和时间上报的连续性。但同时也要留意平台服务端有可能对playbackRate不管但会记录“实际经过时间”。你 8 倍速看一个 20 分钟的视频实际只花 2.5 分钟服务端如果统计观看时长小于视频时长一样会判定不足。所以倍速的极限通常在 2 倍到 4 倍之间再高就要碰运气。4.3 一种实用的“混合策略”如果既想让课程记录看起来合理又不想真的盯着屏幕可以考虑下面的混合流程进入视频页后先把playbackRate设为 4。不要直接跳到结尾而是正常播放到 99%。在接近末尾前把playbackRate调回 1。让视频自然结束触发平台的“完成上报”。这样做的好处是全程都没有异常的currentTime跳变也没有伪造的ended事件。平台收到的数据是“一个倍速播放但连续看完的视频”大部分系统不会对超过 2 倍的播放速度做硬性拦截。但再强调一遍本文提到的所有策略都只能作为测试场景下的技术验证。真实课程是否有服务端风控、是否允许倍速完成不同平台差别很大请自行判断。5. 常见问题与排查技巧实录这部分我把自己实际调试自动化脚本时踩过的坑列一下大多数都是“看似改成功了、实际上没生效”的问题。5.1 currentTime 赋值后又被重置现象执行video.currentTime video.duration之后进度条确实闪了一下到结尾但 1 秒内又回到原来的位置。原因播放器内部保存了原始播放位置或者在监听timeupdate时对异常跳变做了回拉。这种情况下直接赋值没用。排查思路先看播放器是不是原生 HTML5 播放器。如果是平台自研的播放器很可能在内部封装了自己的状态管理不直接暴露给 DOM 层。这时候用原生 API 不一定生效得看它有没有暴露全局播放器对象比如window.player、window.videoPlayer之类。5.2 找不到 video 元素现象控制台执行document.querySelector(video)返回null。原因视频播放页可能用了 iframe 嵌套或者播放器组件是通过 Shadow DOM 渲染的。也就是说常规 DOM 查找看不见。排查思路先确认页面里有没有 iframe。如果有依次进入每个 iframe 的contentDocument里查找。如果怀疑是 Shadow DOM就需要用下面这种穿透方式function findDeepVideo(root) { if (root root.querySelector) { const direct root.querySelector(video); if (direct) return direct; } const walker document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); while (walker.nextNode()) { const node walker.currentNode; if (node.shadowRoot) { const found findDeepVideo(node.shadowRoot); if (found) return found; } } return null; }这个方法会递归穿透所有自定义元素内部的 Shadow DOM比只查一层能覆盖的场景多得多。5.3 上报接口返回 403 或未完成现象前端状态已经显示 100%但刷新学习进度列表仍然显示未完成。原因大概率是服务端在提交完成记录时校验了当前用户的会话状态可能你的登录态已经过期也可能服务端认定本次观看时长过短直接拒了。排查思路在 Network 面板看最后一条请求确认返回值的 message。如果提示“学习时长不足”那基本没救只能挂机或倍速。如果提示“未登录”重新登录后再跑脚本。5.4 完成按钮根本不出现现象视频明明播到了结尾但没有“完成学习”之类的按钮。原因有些平台自动上报完成不需要按钮。有些平台则要求你手动提交按钮出现在“课程任务列表”里而不是视频播放页内。排查思路这个不要硬在播放页找按钮直接刷新课程任务列表页看学习进度有没有同步。5.5 后台切走后视频暂停脚本失效现象把浏览器切到后台过几分钟回来看视频进度卡住了。原因浏览器为了省电会冻结后台标签页里的定时器导致setInterval不再执行。排查思路尽量让页面保持前台状态或者在浏览器设置里关闭后台节流。如果你是非要后台跑的测试场景建议把窗口最小化而不是切到其他标签页。5.6 常见问题速查表现象最常见原因建议处理方式找不到 videoiframe或Shadow DOM递归查找或直接在 iframe 框架中注入脚本进度被重置播放器内部状态回拉改用模拟观看而不是跳转完成记录不更新服务端时长校验倍速播放到结尾不要秒完按钮不出现上报由后端自动完成刷新课程页查看进度后台后停止浏览器节流保持前台或关闭省电模式请求报错登录态失效重新登录后再执行6. 关于这件事我最后想说的我在本地测试环境里跑这些脚本的时候最大的感触是“前端能做的东西远远超过很多人想象但前端能控制的东西也远远低于很多人预期”。一个播放器进度条、一个完成按钮看起来都是前端的事可一旦涉及学习记录的有效性服务端才是最终拍板的人。所以如果你只是想写个脚本逗自己玩研究一下 DOM 事件派发、异步上报、播放器状态管理这些都很有意思。但如果你动了“不学习又想拿学分”的念头请务必停一下因为现在的平台风控并不是摆设。更重要的是技术能力用在这种地方浪费了。我自己的做法是给本地测试课程写自动化脚本用来验证课程系统本身的完成逻辑比如测试提交完观看记录后下一次刷新会不会正确回显。这些脚本让我对浏览器自动化有了更深的理解也踩了不少坑。希望你也能把这些 JavaScript 技巧用在正经开发和小工具上而不是总想着怎么投机取巧。