☰
前端自动化模拟视频学习完成:原理、脚本与反制规避
2026/10/15 21:45:30 网站建设 项目流程

如果你在读这篇文章之前只是听说过“前端自动化可以刷网课进度”,但一直没搞明白里面到底怎么回事,那这篇应该能帮上忙。我这里直接用 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 后端到底校验什么

很多人以为改个前端状态就算完,实际上后端往往会做以下几类校验:

  1. completed字段是否出现。这是最基础的校验,前端上报“完成”,后端就更新记录。
  2. 观看时长是否合理。一个 30 分钟的课程,如果你只用了 10 秒,后端日志里会有明显异常。
  3. 进度连续性是否成立。比如你上一次上报在第 269 秒,下一次直接报第 1800 秒,中间没有任何过渡记录,这就不像是真人观看。
  4. 会话与身份是否一致。包括 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 先摸清页面结构

动手写脚本之前,建议先花两分钟做三件事:

  1. 打开视频页面,按 F12 进入开发者工具。
  2. 在 Elements 面板里搜索video标签,确认其出现位置和父子结构。
  3. 在控制台里输入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 变化。这时就用到了MutationObserver:

const 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 模拟观看的核心思想

如果你的目标不是“一秒完成”,而是“在不长时间占用自己精力的情况下,让学习记录尽可能合理”,那更好的思路是模拟真实观看过程。

具体来说,平台希望看到的时间线是这样的:

  1. 视频从第 0 秒开始播放。
  2. 每隔几秒上报一次位置。
  3. 播放位置持续增加,直到接近视频末尾。
  4. 触发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 一种实用的“混合策略”

如果既想让课程记录看起来合理,又不想真的盯着屏幕,可以考虑下面的混合流程:

  1. 进入视频页后,先把playbackRate设为 4。
  2. 不要直接跳到结尾,而是正常播放到 99%。
  3. 在接近末尾前,把playbackRate调回 1。
  4. 让视频自然结束,触发平台的“完成上报”。

这样做的好处是全程都没有异常的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 技巧用在正经开发和小工具上,而不是总想着怎么投机取巧。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询