Python+Selenium+JavaScript:在线课程学时自动管理实战

发布时间:2026/10/17 3:14:38
Python+Selenium+JavaScript:在线课程学时自动管理实战
有一段时间我需要完成某高校在线学习平台上的一门课程。那个平台的播放器设计得很“贴心”——视频播放一段时间后如果检测到浏览器窗口失去焦点就会自动暂停。白天工作一忙我经常把电脑丢在角落里干别的回来才发现课程进度几乎没动。手动守着吧又实在是浪费时间一节课两个小时总不能真盯着屏幕看两小时。于是我花了一个周末用 Python 和 Selenium 写了一套智能学时管理脚本配合 JavaScript 注入把“盯学时”这件事彻底自动化了。这篇文章想把这套脚本从设计思路、技术选型到落地实现完整复盘一遍。适合已经会一点 Selenium 基础、想挑战真实场景项目的朋友参考也适合被学时任务折磨过的人看个热闹。先泼一盆冷水这套脚本只在合法前提下才有意义。它帮你打点的是“机械点击、状态确认、学时记录”这些重复劳动不是帮你偷懒作弊。用之前先看一眼你所在平台的服务条款如果平台明确不允许自动化访问那请自觉关掉。我这里聊的一切都默认你用的是自己账号下正常注册的课程。1. 学时任务的真实痛点与整体设计思路1.1 在线课程学时统计的两种常见机制写脚本之前得先搞清楚平台到底怎么计算学时。不同平台背后的逻辑差别很大写错了方向整个脚本就废了。我总结下来绝大多数平台按以下两种方式之一统计按“页面在线时长”统计只要你的浏览器一直开着课程页面一段时间后就给你计一笔学时。这种最简单挂个页面就行甚至不需要视频真的在播。按“播放器真实进度”统计课程视频得真的播放到结束播放器上报progress事件后端才记录学时。这种最复杂你的脚本必须保证视频处于真实的播放状态任何暂停、缓冲、异常中断都会导致学时停摆。实践中重要课程基本都采用第二种因为第一种太容易被薅羊毛。这也意味着脚本的核心目标不是“保持页面活着”而是“保持视频真的在播”。另外很多平台还有一套“活跃度检测”逻辑。它们会记录鼠标轨迹、点击间隔、滚动行为、页面焦点等信号判断当前是不是一个真人在操作。如果你全程零操作或者操作频率固定得像机器某些平台会直接掐断会话甚至标记异常。这也是脚本设计里必须考虑的约束条件。1.2 为什么是 Python Selenium JavaScript 的组合标题里写了“PythonJS”不是凑数是三层职责的拆解Python 层大脑负责整体调度、状态机转换、学时数据记录、异常处理和日志输出。它决定脚本“下一步该干什么”。Selenium 层手脚负责驱动真实浏览器模拟点击、滚动、等待页面加载、切换课程章节。它负责“怎么去操作页面”。JavaScript 注入层神经负责直接读取播放器状态、监听 DOM 变化、获取视频的currentTime和duration等数据。它负责“感知页面里发生了什么”。为什么要注入 JS 而不全用 Selenium 的 API因为 Selenium 本质上只能看到 DOM 层的东西很难拿到播放器内部的运行时状态。比如你要判断“视频是否在播放”Selenium 能告诉你页面上有没有一个video元素但它很不容易告诉你这个视频的播放进度是多少、是不是处于缓冲卡顿状态。相反用driver.execute_script()注入一段原生 JavaScript直接读取浏览器内部的播放器对象又快又准几毫秒就能拿到结果。1.3 候选方案对比Requests、Selenium、Playwright 怎么选动手之前我简单对比过几条路线。方案优势劣势适用场景Requests 直接模拟 HTTP轻量、速度快、资源占用小完全无法执行 JS面对前端渲染页面的分析和逆向成本极高接口签名一变就失效纯接口型任务且你已经分析清楚所有请求参数Selenium驱动真实浏览器兼容性最好资料最多速度相对慢资源占用略高本文场景的正解老平台兼容性最稳Playwright速度更快API 友好自动等待机制出色某些历史包袱重的平台会出现定位兼容性问题资料相对少新版平台优先从零搭建新项目时推荐我最终选了 Selenium一个很实际的原因是课程平台是老框架改的页面上有几个很“怪”的 JS 事件绑定Playwright 对这类场景总会踩到莫名其妙的坑。Selenium 因为用得久社区里几乎所有坑都有人蹚过查资料一查一个准。如果你是在新平台上用Playwright 完全可以作为备选。2. 环境搭建与浏览器配置的踩坑实录2.1 依赖安装用 webdriver-manager 省掉版本地狱先给环境下个定义Python 3.9Chrome 浏览器版本随意建议用稳定版然后装依赖。pip install selenium webdriver-manager很多教程会教你手动下载 ChromeDriver 然后扔到系统 PATH 里。听起来没什么实际做起来非常痛苦——Chrome 每次一升级ChromeDriver 就罢工你得去版本对照表找到对应版本、重新下载、重新配置。我早期这么干过后来换了webdriver-manager世界清净了。它会自动检测你本机 Chrome 的版本然后到官方源拉一个匹配的 driver。2.2 ChromeDriver 版本不匹配一个绕不开的坑如果你用的是手动装 driver 的方式很可能会遇到下面这个报错WebDriverException: Message: session not created: This version of ChromeDriver only supports Chrome version 118 Current browser version is 120.0.6099.109原因是 ChromeDriver 的每个大版本号只匹配对应大版本的 Chrome比如 118 的 driver 配 120 的浏览器就是不行。网络一不好、自动更新一跑你昨晚还能跑的脚本今早就崩了。解决办法有两个用 webdriver-manager 自动匹配推荐固定浏览器版本停止 Chrome 自动更新让浏览器和 driver 永远保持一致我自己的方案更简单粗暴写脚本时先用webdriver_manager自动拉取如果网络拉不动就手动指定一个已经下载好的 driver 路径做成配置项。平时跑脚本永远走自动匹配。2.3 浏览器初始化配置哪些参数该加、哪些千万别加下面是初始化 driver 的完整代码我加了注释。from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager options webdriver.ChromeOptions() # 指定用户数据目录关键中的关键登录态持久化之后不用每天扫码 options.add_argument(--user-data-dir/Users/yourname/.auto_course/chrome_profile) # 最大化窗口避免页面响应式布局把播放按钮挤到可视区域外 options.add_argument(--start-maximized) # 关闭“Chrome正受到自动测试软件控制”的提示条界面干净些 options.add_argument(--disable-infobars) # 禁用通知弹窗有些课程平台会推送课程提醒遮挡页面元素 options.add_argument(--disable-notifications) # 禁用 GPU 加速某些老显卡会引发渲染异常实际影响不大但保险 options.add_argument(--disable-gpu) # 忽略证书错误学校代理环境偶发证书劫持 options.add_argument(--ignore-certificate-errors) # 指定 driver 路径优先用 webdriver-manager 自动匹配 service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice, optionsoptions)这里有一个非常实用的心得给 user-data-dir 配一个独立的目录路径。这样浏览器会把 cookie、localStorage、session 等全部持久化到本地目录第一次扫码登录之后后续启动脚本直接就是登录状态几乎可以做到“零交互”。这比我见过有些脚本每次启动都要重新走一遍登录流程高到不知道哪里去了。还有一个反向提醒网上有些教程让你隐藏“webdriver 痕迹”比如修改navigator.webdriver属性、注入 stealth 脚本什么的。我实测下来这些操作对你的正常学时管理脚本来说毫无必要甚至可能帮倒忙。因为这些防检测脚本的行为模式本身就很固定平台的风控系统也不是傻子反而可能因为你的操作轨迹“太完美”而被判定异常。老老实实模拟一个真人的操作节奏反而是最稳妥的。2.4 为什么不建议开无头模式无头模式headless跑起来内存占用小、不占桌面听起来很适合挂机。但我不建议你开原因有三个无头模式下页面渲染行为和真实浏览器有差异某些播放器可能加载不出视频流。平台风控对无头模式的特征识别很容易它本质上为了自动化而生该类特征很难完全抹掉。脚本跑挂了看不出现场排查问题时很痛苦。你挂学时本来就不需要盯着屏幕开着窗口放后台完全不影响你干别的。所以有头但不干扰你工作是这个场景最合理的选择。3. 脚本核心实现从登录到自动续播3.1 登录环节别让登录变成每天的麻烦登录的关键不是写得多炫而是尽量少登录。有了前面配置的--user-data-dir真实流程基本是第一次运行时打开登录页面 → 扫码 → 后续所有启动都自动保持登录态。如果平台只有账密登录那就用显式等待填表单。注意一定不要用time.sleep(固定秒数)去等页面因为网络波动时等不够就报错等太久浪费时间。要这样写from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def login(username, password): driver.get(https://mylms.example.edu.cn/login) # 显式等待账号输入框出现最多等 10 秒 wait WebDriverWait(driver, 10) acct_input wait.until(EC.presence_of_element_located((By.ID, username))) acct_input.clear() acct_input.send_keys(username) driver.find_element(By.ID, password).send_keys(password) driver.find_element(By.ID, login_btn).click() # 等待跳转到首页 wait.until(EC.title_contains(我的课程))登录之后就再也不用管了第一次登录会自动在 user-data-dir 里留下会话第二次再跑脚本时直接跳到“我的课程”页面就行。3.2 播放器状态感知实时读取关键数据这是脚本核心中的核心。通过execute_script注入一段 JavaScript在页面上下文里读取video元素的状态。我把这段探测脚本封装成一个函数def get_video_state(driver): check_script (() { const v document.querySelector(video); if (!v) return null; return { hasSrc: !!v.currentSrc, currentTime: v.currentTime, duration: v.duration, paused: v.paused, ended: v.ended, readyState: v.readyState, bufferEnd: v.buffered.length ? v.buffered.end(v.buffered.length - 1) : 0 }; })(); try: return driver.execute_script(check_script) except Exception: return None这短短几个字段每个都对应一个判断逻辑hasSrc视频有没有加载出播放源。如果为 False说明页面卡在加载阶段。currentTime和duration当前播放位置和总时长。currentTime duration - 1说明快看完了。paused是否暂停。如果为 True说明播放器被自动暂停需要处理。readyState视频加载状态数字越大越就绪。0表示一无所知1表示只有元数据2表示当前帧有了3表示至少能播两帧4表示可以流畅播放。bufferEnd缓冲区末端位置。如果它和currentTime靠得很近说明视频卡在缓冲得等一等或刷新。拿到这些状态Python 层就能做决策了“在播”就记录进度“暂停”就尝试恢复“缓冲”就等待重试。这个数据流是我整套脚本的核心骨架。3.3 自动续播与学时统计主循环和关键代码接下来是整个脚本的主循环逻辑不复杂探测当前页面播放器状态。状态正常且在播放就读取currentTime并写入本地统计。如果发现暂停先尝试直接用 JS 调play()不行就退化为点击页面上的播放按钮。每轮循环之间随机等待一段时间模拟真实观看节奏。import time import random import json def try_play_video(driver): # 先尝试原生 play()很多平台的自定义播放器底层就是 video 元素 js_play (() { const v document.querySelector(video); if (v v.paused) { v.play().then(() true).catch(() false); } return false; })(); driver.execute_script(js_play) time.sleep(2) state get_video_state(driver) if state and not state[paused]: return True # 原生 play() 有时会被平台拦截需要点击页面上的播放按钮 # 不同播放器的按钮样式不同写多个备选定位器轮询尝试 play_selectors [ .play-button, .vjs-play-control, .prism-play-btn, .video-player .play-btn, button[aria-label*播放], ] for selector in play_selectors: try: el driver.find_element(By.CSS_SELECTOR, selector) if el.is_displayed() and el.is_enabled(): el.click() return True except Exception: continue return False def run(driver, course_urls): progress_file study_progress.json data {} # 每轮循环检测一次间隔控制在 8~15 秒之间接近真实观看的检查频率 for url in course_urls: driver.get(url) time.sleep(5) while True: state get_video_state(driver) if state and state[hasSrc] and not state[paused]: # 记录进度课时名 - 播放秒数 key driver.title if key not in data: data[key] 0 # 两次检查间隔太久会导致统计偏差纠偏为取当前播放进度 data[key] state[currentTime] save_progress(progress_file, data) elif state and state[paused]: # 尝试恢复播放 if not try_play_video(driver): break elif state and state[bufferEnd] - state[currentTime] 1: # 缓冲跟不上等一会儿同时防止死循环 time.sleep(5) else: # 状态异常停止当前课程跳到下一个 break if video_finished(state): break time.sleep(8 random.random() * 7)这段代码里有几个小心思值得说明。第一个是统计口径。我直接用currentTime作为学时的记录值而不是每次累加固定的秒数。原因很简单如果视频在第 2 秒暂停了之后每次循环都加 10 秒那统计就是假的跟实际进度对不上。直接记录当前播放时间天然就是精准的程序崩溃了重跑也能从断点继续。第二个是播放按钮的定位策略。我写了好几个备选选择器因为不同课程页面可能用了不同的播放器皮肤。这些选择器有的是播放器通用类名有的接底层的控制条类名还有的走aria-label。从上到下一个个试总有一个能命中。这种“多策略兜底”在自动化里太重要了写死一个选择器基本等于埋雷。第三个是循环节奏。我设置了 8 秒到 15 秒之间的随机间隔。这不仅仅是技术上的妥协也是行为上的考虑。频率太高会显得很“机械”频率太低又会影响学时上报的实时性。实测下来 8~15 秒的区间比较安全。写这个脚本的初衷是代替重复劳动而不是去挑战平台风控这一点要时刻记住。3.4 进度持久化用 JSON 文件记录学时学时记录最终要落盘不能只存在内存里否则脚本一挂全丢。我用一个 JSON 文件来存储结构非常简单{ 2025-05-06 14:23:11: { Python程序设计: 3521, 数据库原理: 1892 } }每次循环只更新当前页面对应的进度值文件尺寸很小读写都是微秒级没有任何性能压力。更重要的是脚本跑完一节课后你打开这个文件就能看到每个课程的累积学时后续如果要做一个日报统计也非常方便。4. 常见故障排查与合理使用节奏4.1 常见故障速查表脚本写得再稳跑起来也免不了遇到问题。我把实操中碰到的几个典型故障整理成了表格方便你对着排查。症状可能原因解决办法报错找不到元素Boolean(id...)页面还没加载完或者元素在 iframe 里用WebDriverWait代替sleep如果有 iframe 就先switch_to.frame()再定位视频进度一直停在某个数字不动播放器切到了缓冲状态readyState 3先判断bufferEnd是否接近currentTime是则等待 10 秒持续卡住就刷新页面session not created异常Chrome 和 ChromeDriver 版本不匹配用webdriver-manager自动拉取对应版本运行几个小时后页面卡死页面内 JS 累积的内存泄漏主循环中加异常捕获超过 2 小时自动重启浏览器进程播放器没有声音画面但显示在播某些平台会做“静音续播”检测这是平台主动防挂机遇到这种情况关掉脚本手动学习别折腾课程有时有临时弹窗挡住播放按钮页面右下角弹出助教通知启动时提前关闭.popup、.notice之类的元素每隔 20 分钟巡检一次4.2 一个重要提醒遇到验证码就停脚本跑着跑着某次启动后页面突然弹出滑块验证码这时候千万别去自动化破解。说实话也没必要。验证码的出现大概率意味着两种可能一是你的网络环境异常比如 IP 频繁变动二是平台检测到高频访问进入了风控状态。正确的姿势是脚本检测到验证码元素后直接暂停退出然后你自己手动操作一下或者换个网络环境过一阵子再跑。我一贯的原则是脚本只做正常用户会做的操作。对于验证码、答题、人脸识别这类平台主动布下的验证手段一律不做处理。这不是技术上的妥协而是给自己划的一条清晰边界。4.3 合理使用节奏像真人一样学习写这套脚本时我给自己订了几条使用规则也建议你参考单日完成的学时总量不超过 4 小时。真人不可能连续高效地学 12 小时课。每跑 1 到 2 个小时主动停 20 分钟让浏览器和网络连接都休息一下。同一个课程一天只完整跑一遍。第二遍就真的需要你自己投入了。遇到任何弹窗、验证码、异常验证一律退出脚本处理坚决不“硬刚”。脚本只用于你自己账号下正常注册的课程不碰任何人的学习数据。这些规则不是为了讨好平台是为你自己好。学习系统的数据会被用来做教学改革分析你在这套系统里留下的行为模式最终会影响课程设计和考核方式。造假数据损害的是后续同学的学习体验这个账算不得。5. 从“能跑”到“好用”四个值得一试的进阶方向5.1 学时日报与数据可视化原始的 JSON 文件只能自己打开看不够直观。进阶一点的做法是在脚本里加一个统计函数每天定时生成一份当日学时报告输出到终端课程Python程序设计今日完成 1.5 小时本周累计 6.5 小时 课程数据库原理今日完成 45 分钟距目标还差 15 分钟再进一步可以用 matplotlib 画一张周学时柱状图。虽然听起来很工程化但看到自己在每一门课上的投入分布时对学习节奏的感知会清晰很多。5.2 异常通知脚本自己会“喊人”长时间挂机时你不可能一直盯着终端。给脚本加一个通知模块是性价比最高的升级。思路是当检测到以下三种情况时主动往你的即时通讯工具推一条消息某个课程连续 10 分钟无法恢复播放遇到验证码脚本自动暂停今日目标学时已达成实现方式不复杂找一个支持 Webhook 的即时通讯工具封装一个send_message(text)函数在关键节点调用即可。活不用干得漂亮但人得能睡个安稳觉。5.3 从命令行脚本到页面控制台命令行脚本最大的短板是操作不直观。你可以用 FastAPI 搭一个非常简单的本地 Web 页面展示当前脚本运行状态、剩余课程列表、今日学时统计。这个进阶方向我试过工作量不大但体验提升非常明显。打开浏览器就能看到脚本现在在跑哪门课、还有多久完成任务比起黑洞洞的终端舒服太多了。5.4 把学习计划纳入调度最后一步是给脚本加一个课程表。你可以在 JSON 文件里配置每个课程的学习时段比如周一三五学 Python周二四学数据库。脚本启动后自动读配置按计划执行学完一门自动切下一门。到这一步它已经从“挂学时工具”变成了一个真正的“学时管家”。写在最后这套脚本写完之后我实际用了将近一个学期。最真实的体会是它帮我省下的不是“点击播放”那几分钟而是每天做决定的精力。以前我打开课程页面总是先看一眼时长犹豫要不要看一犹豫就拖到了晚上。现在脚本一到时间就自动跑起来到了点就自动停进度清清楚楚写在文件里。我只需要做一件事在它跑完之后打开那门课把视频里的内容真正学一遍。最后分享一个实操小技巧给脚本里所有网络请求加一个统一的超时配置比如页面加载超时 15 秒元素等待超时 10 秒。遇到网络抖动时不要立即报错退出先记录现场再让主循环重试两轮。这两行配置能救你整个学期的挂机体验。这个项目的后续扩展点上面写的四个方向都值得挨个试一遍。等你跑通了第一版脚本再回来看这篇文章你会有不一样的收获。