
1. 从“刷课”这件事说起为什么需要脚本辅助“快速刷党课bjut版防暂停”这个标题一看就是校园场景下的产物。bjut是北京工业大学的英文缩写党课则是很多高校学生必须完成的学习任务。这类课程通常以视频形式呈现平台会记录观看进度有的还会随机弹出验证问题或者检测页面是否处于活跃状态。如果只是老老实实从头看到尾一门课动辄几十分钟甚至几个小时几门课叠加起来时间成本相当高。于是就有了“脚本辅助”这个需求。所谓脚本本质上就是一段自动化代码它模拟人的操作替你去点击、去等待、去切换。油猴脚本是其中最常见的一种形式它运行在浏览器里通过Tampermonkey这类扩展管理器加载能够修改网页的行为。标题里的“防暂停”三个字是核心痛点——很多学习平台会检测你是否还在看如果鼠标不动、页面失焦、或者长时间没有交互视频就会自动暂停进度就卡住了。脚本要解决的就是这个问题让视频在后台也能持续播放不被平台的检测机制打断。这个内容适合谁看一是正在被党课学习任务困扰的高校同学二是对油猴脚本、浏览器自动化感兴趣想拿一个真实场景练手的初学者。需要说明的是我在这里讨论的是技术实现思路和脚本编写方法目的是帮助大家理解浏览器脚本的工作原理以及如何用合法合规的方式提高自己的学习效率。至于具体怎么用每个人应该根据自己的实际情况来判断。提示任何自动化工具的使用都应当遵守所在平台的用户协议和学校的规定。本文重点在于技术原理分析和脚本编写教学不鼓励任何违反规定的行为。2. 油猴脚本的核心机制与防暂停原理拆解2.1 油猴脚本到底是怎么“注入”页面的油猴脚本的运行依赖浏览器扩展Tampermonkey或者Violentmonkey、ScriptCat等同类工具。安装扩展之后你可以在里面新建一个用户脚本脚本头部有一段元数据块用// UserScript和// /UserScript包裹里面定义了脚本的名称、匹配的网址、运行时机等信息。当浏览器加载一个页面时Tampermonkey会检查当前网址是否匹配某个脚本的match或include规则如果匹配就把脚本代码注入到页面中执行。注入的时机很关键。run-at可以设置为document-start、document-end、document-idle等。document-start表示页面刚开始加载就执行这时候DOM还没构建完适合做一些早期拦截document-end表示DOM构建完成但外部资源可能还没加载完document-idle则是页面基本加载完毕。对于防暂停脚本来说通常需要在视频元素出现之前就开始监听所以document-start或者document-end比较常用。脚本注入之后它和页面共享同一个JavaScript执行环境可以直接访问页面的DOM、修改元素属性、绑定事件监听器、调用页面上的函数。这就是油猴脚本强大的地方——它不像浏览器插件那样运行在隔离环境中而是直接“住”在页面里想怎么改就怎么改。2.2 视频平台检测“你是否在看”的几种常见手段要防暂停首先得知道平台是怎么判断你在不在看的。根据我接触过的各类在线学习平台常见的检测手段有这么几种第一种是页面可见性检测。浏览器提供了document.visibilityState和document.hidden属性当用户切换到其他标签页或者最小化窗口时这些值会变化。平台监听visibilitychange事件一旦发现页面不可见就暂停视频。这是最基础也最常见的一种。第二种是焦点检测。通过监听window的blur和focus事件判断当前窗口是否处于激活状态。如果你点击了其他窗口blur事件触发平台就可能暂停视频。第三种是鼠标和键盘活动检测。平台会记录你最后一次移动鼠标或按键的时间如果超过一定阈值比如5分钟没有任何交互就认为你不在电脑前自动暂停。第四种是视频元素自身的事件检测。比如监听pause事件如果视频被暂停了就记录状态或者定期检查video.currentTime是否在递增如果长时间不变说明播放卡住了。第五种是定时弹窗验证。随机时间弹出一个小窗口要求你点击确认或者输入验证码不操作就暂停。这种相对少见但确实存在。理解了这些检测手段防暂停脚本的思路就清晰了要么让平台检测不到这些“异常”信号要么在检测到暂停后自动恢复播放。2.3 防暂停脚本的三种技术路线对比针对上面的检测手段脚本可以走不同的技术路线。我把它归纳为三种各有优劣技术路线核心思路优点缺点适用场景事件拦截型重写visibilitychange、blur等事件的监听逻辑让平台的回调失效从源头阻断效果好需要精准定位平台的监听代码可能随平台更新失效检测手段单一的平台状态伪装型定期修改document.visibilityState、模拟鼠标移动事件实现简单不依赖平台代码结构可能被更复杂的检测识破检测手段较弱的平台自动恢复型监听视频的pause事件一旦暂停就立即调用play()通用性强不关心平台怎么检测可能与平台的暂停逻辑产生冲突需要处理Promise大多数场景实际写脚本的时候往往是几种路线组合使用。比如先用状态伪装让平台以为你在看同时用自动恢复兜底万一还是被暂停了立刻恢复播放。标题里的“防暂停”三个字核心就是这套组合拳。注意自动恢复型脚本在调用video.play()时现代浏览器会返回一个Promise。如果视频没有静音自动播放可能被浏览器策略阻止导致Promise被reject。所以脚本里通常需要把视频静音或者捕获异常后延迟重试。3. 手把手写一个防暂停油猴脚本3.1 脚本元数据与基础框架搭建打开Tampermonkey的管理面板点击“添加新脚本”你会看到一个默认的模板。我们把它改成下面这样// UserScript // name 党课防暂停辅助脚本 // namespace http://tampermonkey.net/ // version 1.0 // description 防止党课视频因页面切换或长时间无操作而暂停 // author 你的名字 // match *://*.example.edu.cn/* // grant none // run-at document-start // /UserScript (function() { use strict; // 脚本主体写在这里 })();几个关键点说明一下。match决定了脚本在哪些网址上生效你需要把它改成实际学习平台的域名。grant none表示脚本不需要调用Tampermonkey提供的特殊API直接在当前页面环境运行。run-at document-start让脚本尽早执行这样可以在平台的检测代码绑定之前就做好拦截准备。外面的(function() { ... })()是一个立即执行函数作用是创建一个独立的作用域避免脚本里的变量污染页面的全局环境。use strict开启严格模式让代码更规范。3.2 拦截页面可见性变化的核心代码页面可见性检测是最好拦截的因为浏览器只提供了有限的几个API。平台要监听visibilitychange事件我们可以重写document.addEventListener让平台注册的回调被我们“掉包”。(function() { use strict; // 保存原始的addEventListener const originalAddEventListener document.addEventListener; // 重写addEventListener document.addEventListener function(type, listener, options) { if (type visibilitychange) { // 拦截visibilitychange事件不注册平台的监听器 console.log([防暂停] 已拦截visibilitychange监听); return; } // 其他事件正常注册 return originalAddEventListener.call(this, type, listener, options); }; // 同时重写document.onvisibilitychange属性 Object.defineProperty(document, onvisibilitychange, { get: function() { return null; }, set: function() { console.log([防暂停] 已拦截onvisibilitychange属性设置); } }); })();这段代码的逻辑是平台想通过document.addEventListener(visibilitychange, callback)来监听页面切换我们把addEventListener重写遇到visibilitychange就直接返回不真正注册。这样平台的回调永远不会被触发它也就不知道你切走了页面。同样的思路可以用于window上的blur和focus事件const originalWindowAddEventListener window.addEventListener; window.addEventListener function(type, listener, options) { if (type blur || type focus) { console.log([防暂停] 已拦截 type 监听); return; } return originalWindowAddEventListener.call(this, type, listener, options); };不过要注意有些平台可能用window.onblur function(){}这种赋值方式那就需要额外重写window.onblur和window.onfocus属性。还有一种情况是平台用了document.addEventListener但事件名不是标准的或者用了第三方库封装过那就需要具体分析。3.3 模拟用户活动与视频自动恢复拦截事件只能防住一部分检测更稳妥的做法是主动模拟用户活动同时监听视频状态一旦暂停就恢复。模拟鼠标移动很简单创建一个MouseEvent然后定期派发function simulateActivity() { const event new MouseEvent(mousemove, { bubbles: true, cancelable: true, clientX: Math.random() * window.innerWidth, clientY: Math.random() * window.innerHeight }); document.dispatchEvent(event); } // 每30秒模拟一次鼠标移动 setInterval(simulateActivity, 30000);视频自动恢复的逻辑是监听pause事件然后调用play()function setupVideoWatcher() { const videos document.querySelectorAll(video); videos.forEach(video { if (video.dataset.antiPauseAttached) return; video.dataset.antiPauseAttached true; video.addEventListener(pause, function() { console.log([防暂停] 检测到视频暂停尝试恢复); // 静音以避免自动播放被浏览器阻止 video.muted true; const playPromise video.play(); if (playPromise ! undefined) { playPromise.catch(err { console.log([防暂停] 恢复播放失败, err); // 延迟重试 setTimeout(() { video.play().catch(() {}); }, 1000); }); } }); }); } // 页面加载后查找视频元素 window.addEventListener(load, setupVideoWatcher); // 有些平台视频是动态加载的用MutationObserver监听DOM变化 const observer new MutationObserver(setupVideoWatcher); observer.observe(document.body, { childList: true, subtree: true });这里有几个细节值得展开说。第一video.muted true是为了绕过浏览器的自动播放策略。现代浏览器规定如果视频没有静音必须要有用户交互才能自动播放。脚本模拟的play()调用不算用户交互所以会被阻止。静音之后就可以自动播放了。第二play()返回Promise如果失败会reject我们需要捕获并重试。第三有些平台的视频是动态插入DOM的所以要用MutationObserver持续监听发现新视频就绑定监听器。3.4 处理定时弹窗与验证问题有些平台会随机弹出验证窗口要求你点击确认。这种就没法用简单的防暂停逻辑处理了需要监听弹窗的出现然后自动点击。function handlePopup() { // 根据实际平台的弹窗选择器来定位 const popupBtn document.querySelector(.popup-confirm-btn, .verify-btn, [class*dialog] button); if (popupBtn) { console.log([防暂停] 检测到弹窗自动点击); popupBtn.click(); } } // 每5秒检查一次 setInterval(handlePopup, 5000);选择器需要根据实际平台的HTML结构来写。你可以打开浏览器的开发者工具用元素选择器点一下弹窗上的按钮看看它的class或id是什么然后填到querySelector里。如果弹窗是iframe加载的还需要先获取iframe的contentDocument再查找。提示自动点击弹窗按钮存在一定风险如果平台把这种行为判定为作弊可能会记录异常。建议只在确认平台允许的范围内使用或者手动处理弹窗。4. 调试、优化与常见问题排查4.1 用开发者工具验证脚本是否生效脚本写完之后怎么知道它有没有起作用最直接的方法是打开浏览器的开发者工具切换到Console面板看脚本输出的日志。上面代码里的console.log就是为此准备的。如果你看到[防暂停] 已拦截visibilitychange监听这样的输出说明脚本已经注入并执行了。另一个验证方法是手动切换标签页然后回到学习页面看视频有没有暂停。如果没暂停说明拦截生效了。还可以在Console里输入document.visibilityState正常情况下切换标签页后这个值会变成hidden但脚本拦截的是事件监听不会改变这个属性的值所以平台即使读这个属性也拿不到事件通知。如果脚本没生效先检查match规则是否匹配当前网址。Tampermonkey的图标上会显示当前页面有多少个脚本在运行点开可以看到具体是哪些。如果脚本显示已启用但没输出日志可能是run-at的时机不对或者脚本里有语法错误导致执行中断。4.2 脚本冲突与性能优化的注意事项油猴脚本是在页面环境里运行的如果页面上有其他脚本也在操作DOM或重写事件就可能产生冲突。比如你重写了addEventListener另一个脚本也重写了后执行的会覆盖先执行的。解决办法是在脚本里保存原始函数并且在调用时用call或apply确保上下文正确。性能方面MutationObserver和setInterval都是持续运行的操作如果频率太高会消耗CPU。MutationObserver的回调里尽量只做必要的检查不要每次都遍历整个DOM。setInterval的间隔可以设长一点比如30秒一次模拟活动就够了弹窗检查5到10秒一次也足够。还有一个容易忽略的点是内存泄漏。如果页面是单页应用SPA路由切换时旧页面的视频元素可能被移除但监听器还挂在上面。可以在MutationObserver里检查元素是否还在文档中不在就清理掉。4.3 常见问题速查表问题现象可能原因排查方法解决方案脚本完全不执行match不匹配看Tampermonkey图标上的数字修改match为实际域名视频仍然暂停平台用了其他检测手段在Console里搜索pause相关日志补充拦截对应事件自动播放失败浏览器自动播放策略看Console是否有NotAllowedError设置video.muted true弹窗没被点击选择器不对用开发者工具检查弹窗元素更新querySelector选择器页面卡顿定时器频率太高用Performance面板分析降低setInterval频率脚本更新后失效平台改了代码结构对比新旧页面源码重新分析并调整拦截逻辑4.4 我踩过的几个坑第一个坑是过度拦截导致页面功能异常。有一次我把所有addEventListener都拦截了结果平台的其他功能也挂了比如进度保存、章节切换都失效。后来改成只拦截特定事件类型并且加了白名单只拦截来自平台核心脚本的调用。第二个坑是视频恢复后进度不保存。有些平台在视频暂停时会触发进度上报如果脚本立刻恢复播放上报可能还没完成导致进度丢失。解决办法是在恢复播放前加一个短延迟比如500毫秒让平台的上报逻辑先跑完。第三个坑是多视频页面处理不当。有些课程页面有多个视频元素比如主视频加广告视频。如果对所有视频都绑定自动恢复广告视频也会被强制播放。后来我改成只对duration大于一定值比如60秒的视频生效过滤掉短视频。第四个坑是脚本在iframe里不生效。有些平台把视频放在iframe里加载油猴脚本默认只在顶层页面运行。需要在元数据里加match匹配iframe的网址或者用all-frames true让脚本在所有框架里运行。5. 从脚本编写延伸出去的一些思考写这个脚本的过程其实是一次很好的浏览器API实践。你接触到了事件模型、DOM操作、异步编程、浏览器安全策略这些前端核心概念。如果把这些知识迁移到其他场景比如自动化测试、网页数据采集、浏览器插件开发都是通用的。另外脚本的维护成本不容忽视。平台一旦更新选择器和事件逻辑就可能失效你需要重新分析。所以写脚本的时候尽量把平台相关的部分抽离成配置比如选择器、事件名、域名都放在脚本开头方便后续修改。有条件的话可以写一个简单的检测逻辑脚本启动时先验证关键元素是否存在不存在就在Console里给出提示。最后说一点个人体会。技术本身是中性的关键在于怎么用。用脚本节省时间把精力放在真正有价值的学习内容上这无可厚非。但如果只是机械地刷完进度什么也没学到那脚本反而成了自欺欺人的工具。我在实际使用中的做法是对于已经掌握的内容用脚本快速过一遍对于新内容还是老老实实看该记笔记记笔记。工具是为人服务的别被工具牵着走。