news 2026/10/17 3:14:21

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python+Selenium+JavaScript:在线课程学时自动管理实战

有一段时间,我需要完成某高校在线学习平台上的一门课程。那个平台的播放器设计得很“贴心”——视频播放一段时间后,如果检测到浏览器窗口失去焦点,就会自动暂停。白天工作一忙,我经常把电脑丢在角落里干别的,回来才发现课程进度几乎没动。手动守着吧,又实在是浪费时间,一节课两个小时,总不能真盯着屏幕看两小时。

于是我花了一个周末,用 Python 和 Selenium 写了一套智能学时管理脚本,配合 JavaScript 注入,把“盯学时”这件事彻底自动化了。这篇文章想把这套脚本从设计思路、技术选型到落地实现完整复盘一遍。适合已经会一点 Selenium 基础、想挑战真实场景项目的朋友参考,也适合被学时任务折磨过的人看个热闹。

先泼一盆冷水:这套脚本只在合法前提下才有意义。它帮你打点的是“机械点击、状态确认、学时记录”这些重复劳动,不是帮你偷懒作弊。用之前先看一眼你所在平台的服务条款,如果平台明确不允许自动化访问,那请自觉关掉。我这里聊的一切,都默认你用的是自己账号下正常注册的课程。

1. 学时任务的真实痛点与整体设计思路

1.1 在线课程学时统计的两种常见机制

写脚本之前,得先搞清楚平台到底怎么计算学时。不同平台背后的逻辑差别很大,写错了方向整个脚本就废了。

我总结下来,绝大多数平台按以下两种方式之一统计:

  • 按“页面在线时长”统计:只要你的浏览器一直开着课程页面,一段时间后就给你计一笔学时。这种最简单,挂个页面就行,甚至不需要视频真的在播。
  • 按“播放器真实进度”统计:课程视频得真的播放到结束,播放器上报progress事件,后端才记录学时。这种最复杂,你的脚本必须保证视频处于真实的播放状态,任何暂停、缓冲、异常中断都会导致学时停摆。

实践中,重要课程基本都采用第二种,因为第一种太容易被薅羊毛。这也意味着,脚本的核心目标不是“保持页面活着”,而是“保持视频真的在播”。

另外,很多平台还有一套“活跃度检测”逻辑。它们会记录鼠标轨迹、点击间隔、滚动行为、页面焦点等信号,判断当前是不是一个真人在操作。如果你全程零操作,或者操作频率固定得像机器,某些平台会直接掐断会话甚至标记异常。这也是脚本设计里必须考虑的约束条件。

1.2 为什么是 Python + Selenium + JavaScript 的组合

标题里写了“Python+JS”,不是凑数,是三层职责的拆解:

  • 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.9+,Chrome 浏览器(版本随意,建议用稳定版),然后装依赖。

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 的浏览器,就是不行。网络一不好、自动更新一跑,你昨晚还能跑的脚本今早就崩了。

解决办法有两个:

  1. 用 webdriver-manager 自动匹配(推荐)
  2. 固定浏览器版本,停止 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(service=service, options=options)

这里有一个非常实用的心得:给 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 自动续播与学时统计:主循环和关键代码

接下来是整个脚本的主循环,逻辑不复杂:

  1. 探测当前页面播放器状态。
  2. 状态正常且在播放,就读取currentTime并写入本地统计。
  3. 如果发现暂停,先尝试直接用 JS 调play(),不行就退化为点击页面上的播放按钮。
  4. 每轮循环之间随机等待一段时间,模拟真实观看节奏。
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 秒。遇到网络抖动时,不要立即报错退出,先记录现场,再让主循环重试两轮。这两行配置,能救你整个学期的挂机体验。这个项目的后续扩展点,上面写的四个方向都值得挨个试一遍。等你跑通了第一版脚本,再回来看这篇文章,你会有不一样的收获。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 3:14:18

容器网络核心机制与算法:从命名空间到VXLAN与负载均衡

容器网络这个话题&#xff0c;放到技术群里永远是热门。凡是上手过容器的人&#xff0c;十有八九都在这里栽过跟头&#xff1a;容器明明跑起来了&#xff0c;互相之间却 ping 不通&#xff1b;服务都部署好了&#xff0c;负载均衡就是不生效&#xff1b;换一套网络插件&#xf…

作者头像 李华
网站建设 2026/10/17 3:14:13

AI养虾系统实战:边缘计算+云端训练,成功率从65%提升到95%

1. 从“看天吃饭”到“看数据养虾”&#xff1a;AI养虾到底改变了什么对虾养殖这行&#xff0c;老一辈的经验是“看水色、摸虾情、凭感觉投料”。一个老师傅带几个塘&#xff0c;每天凌晨三点起来巡塘&#xff0c;靠手电筒照水面观察虾的活动&#xff0c;靠经验判断要不要增氧、…

作者头像 李华
网站建设 2026/10/17 3:13:51

AI智能化实践报告工具怎么用?从素材整理到复盘一篇讲透

每年实习季结束&#xff0c;总有一批人半夜对着空白文档抓头发。实习总结、实践报告、顶岗实习手册&#xff0c;一个比一个难写。不是没干活&#xff0c;是真的不知道怎么把这三个月的经历落成文字。我自己就是从那个阶段过来的&#xff0c;后来带实习生、帮人改报告&#xff0…

作者头像 李华
网站建设 2026/10/17 3:13:17

PWM调LED亮度原理与实现:占空比、频率和驱动电路全解析

简介&#xff1a;基于STC12C2052单片机的PWM调光LED控制工程&#xff0c;面向电子爱好者、单片机初学者及需要实现LED无级亮度调节的开发者。资源围绕脉宽调制&#xff08;PWM&#xff09;技术展开&#xff0c;通过改变占空比控制LED平均电流&#xff0c;实现从暗到亮的平滑调节…

作者头像 李华
网站建设 2026/10/17 3:13:13

OpenClaw开源AI助手框架:从零部署到自动化操作电脑

这几天我把 OpenClaw 从头到尾完整跑了一遍。说实话&#xff0c;第一次看到项目介绍的时候我是有点懵的&#xff0c;满屏都是 Agent、MCP、SDK 这些词&#xff0c;文档写得又硬核&#xff0c;小白看了确实容易直接劝退。但真正动手摸完一遍之后&#xff0c;我的结论完全变了&am…

作者头像 李华
网站建设 2026/10/17 3:13:12

用规则与脚本终结文件混乱:归档、终端别名与备份实操

我一直觉得&#xff0c;所谓“学习笔记”不应该只是把看到的东西抄一遍&#xff0c;而是要把这一天里真正想明白的事、试过的方法、踩过的坑&#xff0c;全部沉淀下来。这份4月14日的学习笔记&#xff0c;就是这样一个折腾日的产物。起因其实很朴素——手头的东西越来越乱&…

作者头像 李华