Python Selenium自动化挂网课实战:从原理到实现
1. 项目概述:为什么我们需要自动化挂网课?
作为一名常年与代码和数据打交道的程序员,我深知时间就是最宝贵的资源。无论是学生时代为了学分,还是工作后为了完成某些必要的线上培训,被动地坐在电脑前观看冗长的网课视频,都是一种效率极低的时间消耗。手动操作不仅枯燥,还容易因为走神而错过签到或答题,导致前功尽弃。这就是我决定动手用 Python 和 Selenium 来解决这个问题的初衷。
“挂网课”这个需求背后,本质上是将重复、规则明确的网页交互流程自动化。我们需要的不是去破解或攻击平台,而是模拟一个“好学生”的正常操作:登录、找到课程、播放视频、处理可能弹出的互动(如答题、确认弹窗),并在一个视频结束后自动跳转到下一个。Python 作为胶水语言,拥有极其丰富的生态库,而 Selenium 则是浏览器自动化的“瑞士军刀”,它能驱动真实的浏览器(如 Chrome)执行点击、输入、跳转等所有人工能做的操作,完美契合我们的需求。
本次实战,我选择了“经世优学”这个在线教育平台作为案例。它具有一定的代表性,其页面结构、视频播放逻辑和常见的防呆机制(如无操作检测)在很多同类平台中都能找到影子。通过这个案例,你不仅能获得一套可运行的代码,更能掌握一套通用的“侦察-分析-编码-调试”方法论,未来面对其他平台时也能举一反三。无论你是 Python 新手想找个有趣的项目练手,还是被网课所困想解放双手,这篇文章都将提供从环境搭建到代码调试的完整路径。
2. 核心思路与技术选型解析
2.1 自动化挂网课的核心逻辑拆解
在动手写一行代码之前,我们必须把“挂网课”这个模糊的需求,拆解成计算机可以理解的一系列精确步骤。这个过程就像给一个机器人编写工作手册。
首先, 流程梳理 。一个完整的挂课流程通常包括:1) 启动浏览器并打开登录页;2) 输入账号密码完成登录;3) 在课程列表或学习中心定位到目标课程;4) 进入课程章节列表;5) 循环遍历每一个视频节点:a. 点击播放,b. 监控视频播放状态(是否播放、是否卡住、是否结束),c. 处理播放过程中可能出现的弹窗(如“确认继续学习”、“随堂测验”),d. 视频结束后标记为已完成或自动播放下一个。
其次, 状态监控 。这是自动化的核心难点。我们不能简单地点一下播放按钮就放任不管。平台可能有多种防挂机机制:比如,检测页面是否处于前台(即浏览器标签页是否被激活);比如,视频播放到一定时间会弹出选择题;再比如,长时间无鼠标键盘操作会暂停播放。我们的脚本必须能感知这些状态变化并做出正确响应。
最后, 稳健性设计 。网络会波动,页面加载有时快有时慢,元素可能因为页面改版而定位不到。一个健壮的脚本必须包含异常处理、重试机制和足够的等待时间,确保在非理想环境下也能持续运行数小时而不中断。
2.2 为什么是 Python + Selenium?
面对网页自动化,有几个常见的工具选项:Selenium, Playwright, Puppeteer,以及一些桌面自动化工具如 PyAutoGUI。
我选择 Selenium 的原因主要有三点:
- 生态成熟,资料丰富 :Selenium 是历史最悠久的浏览器自动化工具之一,拥有最庞大的社区。无论你遇到多奇怪的问题,几乎都能在 Stack Overflow 或相关博客中找到解决方案。这对于快速开发和排查问题至关重要。
- 跨浏览器与语言支持 :Selenium 支持 Chrome, Firefox, Edge 等主流浏览器。虽然我们主要用 Chrome,但这份兼容性意味着代码有更好的可移植性。同时,它支持多种语言绑定,Python 的
selenium包非常易用。 - 模拟真实浏览器行为 :Selenium 通过 WebDriver 协议驱动真正的浏览器内核。这意味着它产生的流量和行为与真人操作几乎无异,很难被服务器端轻易区分(当然,平台可以通过检测 WebDriver 特征来反爬,但有应对方法)。相比之下,PyAutoGUI 是模拟鼠标键盘,对前端框架复杂的现代网页控制精度不够,且无法读取页面元素状态。
为什么不选 Playwright 或 Puppeteer? 它们都是更现代、性能更好的工具,特别是 Playwright,在自动等待和跨浏览器一致性上做得非常出色。但对于这个特定场景,Selenium 的稳定性和广泛的社区支持是更大的优势。且我们的操作并不需要 Playwright 那些先进的网络拦截或移动端模拟特性。Selenium 完全够用,学习曲线也更平缓。
Python 则是自动化脚本的首选语言,语法简洁,库丰富。除了 Selenium,我们还会用到 time 进行简单等待, random 模拟人类操作间隔, logging 记录运行日志方便排查问题。
3. 环境准备与核心工具详解
3.1 搭建 Python 与 Selenium 环境
工欲善其事,必先利其器。环境配置是第一步,也是最容易踩坑的一步。我会详细说明每一步,确保新手也能顺利搭建。
第一步:安装 Python 如果你还没有安装 Python,请前往 Python 官网下载安装包。 强烈建议选择 Python 3.8 及以上版本 ,并记得在安装时勾选 “Add Python to PATH” 选项,这能避免后续在命令行中找不到 Python 的麻烦。安装完成后,打开命令行(CMD 或 Terminal),输入 python --version ,如果能看到版本号,说明安装成功。
第二步:安装 Selenium 库 Python 的包管理工具 pip 让安装库变得极其简单。在命令行中执行以下命令:
pip install selenium
如果下载速度慢,可以使用国内镜像源,例如:
pip install selenium -i https://pypi.tuna.tsinghua.edu.cn/simple
第三步:下载浏览器驱动(WebDriver) 这是最关键也最容易出错的一步。Selenium 需要通过一个名为“WebDriver”的组件来操控浏览器。你需要下载与你电脑上 Chrome 浏览器版本号匹配 的 ChromeDriver。
- 查看 Chrome 版本 :打开 Chrome 浏览器,点击右上角三个点 -> 帮助 -> 关于 Google Chrome。记下版本号(例如,120.0.6099.109)。
- 下载 ChromeDriver :访问 ChromeDriver 官方下载站或国内镜像站。找到与你的 Chrome 主版本号(例如120)一致的驱动版本进行下载。
- 放置驱动 :下载后是一个可执行文件(如
chromedriver.exe或chromedriver)。你有两种处理方式:- 方式A(推荐) :将其放在一个固定的目录(如
C:\WebDriver\或/usr/local/bin/),然后将该目录添加到系统的环境变量PATH中。这样 Selenium 就能自动找到它。 - 方式B :在代码中指定驱动的绝对路径。这种方式更直接,但不利于代码迁移。
- 方式A(推荐) :将其放在一个固定的目录(如
注意 :务必保证浏览器版本与驱动版本匹配!不匹配是导致
SessionNotCreatedException错误的最常见原因。如果 Chrome 自动更新了,你可能需要重新下载对应版本的驱动。
3.2 分析目标网站:经世优学平台
在编写自动化脚本之前,我们必须化身“侦察兵”,手动操作一遍流程,并用开发者工具(F12)仔细分析页面结构。这是整个项目成败的基础。
手动操作与观察 :
- 打开经世优学平台,手动登录。
- 进入你的课程学习页面。
- 播放一个视频。注意观察:
- 登录表单 :账号和密码输入框的
id或name是什么? - 课程列表 :课程链接是如何排列的?用什么元素(
class,tag)包裹? - 视频播放器 :播放按钮是普通的
button还是div?它的唯一标识是什么? - 进度条与状态 :如何判断视频在播放?是看播放按钮的样式变化,还是页面有特定的状态文本(如“播放中”)?
- 弹窗与互动 :播放过程中,是否有“确认继续学习”、“随堂测验”等弹窗?它们出现有什么规律?关闭按钮如何定位?
- 完成标记 :视频看完后,页面是否会显示“已完成”?或者有一个复选框需要勾选?
- 登录表单 :账号和密码输入框的
使用开发者工具(F12) : 这是我们的核心侦察工具。切换到 Elements 面板,你可以看到网页的 HTML 结构。
- 定位元素 :点击左上角的箭头图标,然后去点击页面上的元素(如播放按钮),代码面板会自动定位到对应的 HTML 代码。你需要找到该元素的 唯一或稳定的选择器 。
- 选择器优先级 :
id>name>class>tag>XPath>CSS Selector。优先使用id,因为它通常是唯一的。如果没有id,则观察class或其它属性。XPath功能强大但可能随页面结构调整而变化,应谨慎使用。 - 网络请求观察 :切换到
Network面板,刷新页面或进行播放操作。你可以看到视频流(可能是m3u8或mp4文件)的请求,以及标记课程完成时触发的 API 请求。理解这些请求有助于我们更精准地模拟“完成”动作。
通过这番侦察,我们就能为每个需要操作的元素找到“地址”,从而在代码中指挥 Selenium 去找到并操作它们。
4. 代码实战:从登录到自动播放
4.1 初始化驱动与登录模块
让我们开始编写代码。首先创建一个新的 Python 文件,比如 auto_course.py 。
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
import time
import random
import logging
# 配置日志,方便查看运行状态和排查问题
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class AutoCourse:
def __init__(self, headless=False):
"""
初始化浏览器驱动
:param headless: 是否使用无头模式(不显示浏览器界面)
"""
options = webdriver.ChromeOptions()
# 添加一些常用选项,使浏览器行为更接近真人
options.add_argument('--disable-blink-features=AutomationControlled') # 隐藏自动化特征
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option('useAutomationExtension', False)
if headless:
options.add_argument('--headless') # 无头模式,后台运行
options.add_argument('--disable-gpu')
# 指定驱动路径(如果已加入PATH,则不需要)
# self.driver = webdriver.Chrome(executable_path='你的驱动路径', options=options)
self.driver = webdriver.Chrome(options=options)
self.wait = WebDriverWait(self.driver, 10) # 设置显式等待,最多等10秒
logger.info("浏览器驱动初始化成功。")
def login(self, username, password, login_url):
"""
登录功能
:param username: 用户名
:param password: 密码
:param login_url: 登录页面地址
"""
logger.info(f"正在访问登录页面: {login_url}")
self.driver.get(login_url)
# 等待页面加载,这里假设登录表单已出现
# 你需要根据实际页面修改选择器
try:
username_input = self.wait.until(
EC.presence_of_element_located((By.ID, "username")) # 替换为实际的ID
)
password_input = self.driver.find_element(By.ID, "password") # 替换为实际的ID
login_button = self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']") # 替换为实际选择器
# 模拟人类输入,有间隔地输入字符
for char in username:
username_input.send_keys(char)
time.sleep(random.uniform(0.05, 0.2))
time.sleep(random.uniform(0.5, 1.0))
for char in password:
password_input.send_keys(char)
time.sleep(random.uniform(0.05, 0.2))
time.sleep(random.uniform(0.5, 1.0))
login_button.click()
logger.info("登录信息已提交,等待页面跳转...")
# 等待登录成功后的页面元素出现,比如用户头像或课程中心入口
# self.wait.until(EC.presence_of_element_located((By.ID, "user-avatar")))
time.sleep(3) # 简单等待,实际应用中应使用上面的显式等待
except TimeoutException:
logger.error("登录页面元素加载超时,请检查网络或选择器。")
self.driver.save_screenshot('login_timeout.png') # 截图保存,方便排查
raise
except Exception as e:
logger.error(f"登录过程中发生未知错误: {e}")
raise
代码解析与注意事项 :
-
__init__方法 :我们创建了一个ChromeOptions对象来配置浏览器。--disable-blink-features=AutomationControlled等参数是为了尽可能隐藏 Selenium 的自动化特征,避免被一些网站检测到。WebDriverWait是“显式等待”工具,比粗暴的time.sleep更智能,它会等待某个条件成立(如元素出现)后再继续,最多等待指定时间。 -
login方法 :这是登录的核心。我们使用find_element和By来定位元素。 关键点在于选择器的准确性 ,代码中的"username","password"等需要你根据实际网页的 HTML 结构进行替换。模拟人类输入(random.uniform控制间隔)是为了增加行为真实性,对抗简单的行为检测。 - 异常处理与日志 :我们使用
try...except捕获可能出现的超时或元素找不到异常,并用logging记录信息。出错时自动截图 (save_screenshot) 是极其有用的调试手段,能让你看到脚本“眼中”的页面是什么样子。
4.2 导航课程与章节列表
登录成功后,我们需要导航到具体的课程和章节。这部分逻辑因平台而异,但思路相通。
def navigate_to_course(self, course_link_selector):
"""
导航到指定课程页面
:param course_link_selector: 课程链接的CSS选择器或XPath
"""
logger.info("正在寻找课程链接...")
try:
# 方法1:如果课程链接可以直接点击
course_link = self.wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, course_link_selector))
)
course_link.click()
logger.info("已进入课程页面。")
time.sleep(2) # 等待课程页面加载
except TimeoutException:
logger.warning("未找到可点击的课程链接,尝试通过URL直接进入...")
# 方法2:如果你知道课程页面的直接URL,可以直接get
# self.driver.get("你的课程直链URL")
# 或者,可能需要先点击“我的课程”、“学习中心”等入口
# 这里需要你根据实际页面结构添加更多导航逻辑
pass
def get_chapter_list(self, chapter_container_selector):
"""
获取课程的所有章节/视频列表
:param chapter_container_selector: 包裹所有章节项的容器选择器
:return: 章节元素列表
"""
logger.info("正在获取章节列表...")
try:
# 等待章节列表容器加载
chapter_container = self.wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, chapter_container_selector))
)
# 假设每个章节项都在容器内,有共同的类名,比如 'chapter-item'
# 你需要根据实际情况调整子元素选择器
chapter_items = chapter_container.find_elements(By.CLASS_NAME, 'chapter-item')
logger.info(f"共找到 {len(chapter_items)} 个章节。")
return chapter_items
except TimeoutException:
logger.error("获取章节列表超时。")
self.driver.save_screenshot('chapter_list_timeout.png')
return []
实操心得 :
- 页面结构多变 :不同平台的课程入口千差万别。你可能需要先点击“学习中心”,再在“进行中的课程”列表里找,或者平台有一个统一的“课程列表”页面。 务必使用开发者工具仔细分析路径 。
- 动态加载 :很多现代网站使用异步加载(Ajax)。你点击“我的课程”后,课程列表可能稍后才渲染出来。这时必须使用
WebDriverWait等待列表元素出现,而不是用固定的time.sleep。 - 列表获取 :
find_elements(注意是复数)会返回一个元素列表。遍历这个列表,就能逐个处理每个章节或视频。
4.3 视频播放与状态监控
这是最核心的部分。我们需要自动播放视频,并判断何时播完,以进入下一个。
def play_video_and_monitor(self, video_item_element):
"""
播放单个视频并监控其状态直至结束
:param video_item_element: 代表一个视频章节的WebElement对象
"""
logger.info("开始处理一个视频章节。")
try:
# 1. 点击进入该视频详情页或直接播放
# 假设视频标题是一个可点击的链接
video_link = video_item_element.find_element(By.CLASS_NAME, 'video-title')
video_link.click()
time.sleep(3) # 等待视频页面加载
# 2. 查找并点击播放按钮
# 播放按钮的选择器需要你仔细分析页面
play_button_selector = "div.video-player button.play" # 示例
play_button = self.wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, play_button_selector))
)
play_button.click()
logger.info("视频开始播放。")
# 3. 监控视频播放状态
video_duration = self.get_video_duration() # 假设有方法获取视频总时长
if video_duration:
logger.info(f"视频总时长约为 {video_duration} 秒。")
# 方法A:基于时间的简单等待(不推荐,无法处理暂停、卡顿)
# time.sleep(video_duration + 10) # 多等10秒缓冲
# 方法B:轮询检查播放进度或完成状态(推荐)
self.monitor_by_progress(video_duration)
else:
# 方法C:如果无法获取时长,则检查“已完成”状态或下一个按钮是否出现
self.monitor_by_completion_flag()
# 4. 视频结束后,可能需要进行“标记完成”操作
self.mark_as_completed()
# 5. 返回章节列表页,准备播放下一个
self.driver.back()
time.sleep(2)
except Exception as e:
logger.error(f"处理视频章节时发生错误: {e}")
self.driver.save_screenshot('video_play_error.png')
# 可以选择跳过当前视频,继续下一个
# 或者尝试刷新页面重试
def get_video_duration(self):
"""
尝试从页面获取视频总时长(秒)
需要根据平台页面实际元素调整
"""
try:
# 示例:时长可能显示在一个类名为 'duration' 的span里,格式为 "12:34"
duration_text = self.driver.find_element(By.CLASS_NAME, 'duration').text
mins, secs = map(int, duration_text.split(':'))
total_seconds = mins * 60 + secs
return total_seconds
except:
logger.warning("无法获取视频时长,将使用备用监控方法。")
return None
def monitor_by_progress(self, expected_duration):
"""
通过轮询播放进度来监控
:param expected_duration: 预期总时长(秒)
"""
logger.info("开始通过进度监控视频播放。")
start_time = time.time()
last_progress = 0
stuck_count = 0
while True:
time.sleep(10) # 每10秒检查一次
elapsed = time.time() - start_time
try:
# 尝试获取当前播放时间(需要根据播放器元素调整)
# 示例:当前时间在一个类名为 'current-time' 的元素里
current_time_text = self.driver.find_element(By.CLASS_NAME, 'current-time').text
current_seconds = self._time_text_to_seconds(current_time_text)
progress = current_seconds
logger.info(f"已播放 {elapsed:.0f} 秒,视频进度 {current_seconds} 秒。")
# 判断是否卡住:进度在两次检查间没有变化
if abs(progress - last_progress) < 2: # 2秒内没变化
stuck_count += 1
logger.warning(f"视频进度可能卡住,计数: {stuck_count}")
if stuck_count >= 3: # 连续3次检查没变化
logger.warning("视频可能已暂停或卡住,尝试点击播放按钮恢复...")
self._click_play_button()
stuck_count = 0
else:
stuck_count = 0
last_progress = progress
# 判断是否结束:当前时间接近总时长,或者页面出现“已完成”标志
if current_seconds >= expected_duration - 5: # 提前5秒判断
logger.info("视频进度已接近总时长,判断为播放结束。")
break
# 防止无限循环
if elapsed > expected_duration * 2: # 如果实际耗时远超预期时长
logger.warning("播放时间已远超预期,强制结束监控。")
break
except NoSuchElementException:
# 可能播放器元素变了,或者视频已结束进入新页面
logger.info("无法找到播放进度元素,可能视频已结束。")
break
except Exception as e:
logger.error(f"监控进度时发生错误: {e}")
# 可以在这里加入更多的恢复逻辑,比如刷新页面
break
def _time_text_to_seconds(self, time_str):
"""将 '12:34' 格式的时间字符串转换为秒数"""
parts = list(map(int, time_str.split(':')))
if len(parts) == 2:
return parts[0] * 60 + parts[1]
elif len(parts) == 3:
return parts[0] * 3600 + parts[1] * 60 + parts[2]
return 0
def _click_play_button(self):
"""尝试点击播放按钮(用于恢复播放)"""
try:
play_btn = self.driver.find_element(By.CSS_SELECTOR, "button.play, div.play-button")
play_btn.click()
logger.info("已尝试点击播放按钮。")
except:
pass
核心难点与解决方案 :
-
如何判断视频结束? 这是最大的挑战。上面代码提供了三种思路:
- 基于已知时长等待 :如果页面显示了视频总长,可以简单等待对应时间。但这种方法无法处理视频卡顿、暂停或中途弹出的测验。
- 轮询播放进度 :不断读取播放器的当前时间,当它接近总时长时判断为结束。更可靠,但需要页面元素暴露当前时间。
- 检测完成状态 :视频播完后,页面可能会出现“已完成”标签、勾选框变亮或“下一节”按钮激活。检测这些元素是最直接的方式。 最佳实践是组合使用 :优先检测“已完成”标志,如果没有,则轮询进度,最后才用固定时长兜底。
-
防挂机检测 :有些平台会检测页面是否处于非活动状态。我们的脚本可以通过 随机间隔执行一些无害操作 来模拟活动,比如轻微移动鼠标(通过 JavaScript)、切换标签页(
driver.switch_to.window)再切回来、或者定时刷新页面。但要注意,刷新可能导致播放进度丢失。
4.4 处理弹窗与异常情况
没有哪个自动化脚本能一帆风顺,我们必须为各种“惊喜”做好准备。
def handle_popups_during_playback(self):
"""
在视频播放过程中,周期性检查并处理可能出现的弹窗
这个方法应该在监控循环中定期调用
"""
popup_selectors = [
("div.confirm-dialog button.confirm", "click"), # 确认继续学习弹窗
("div.quiz-modal", "answer_and_close"), # 随堂测验弹窗
("div.alert button.close", "click"), # 普通警告弹窗
]
for selector, action in popup_selectors:
try:
# 快速查找,不等待,因为弹窗可能不存在
popup = self.driver.find_element(By.CSS_SELECTOR, selector)
logger.warning(f"检测到弹窗,选择器: {selector}")
if action == "click":
# 找到弹窗内的确认或关闭按钮并点击
confirm_btn = popup.find_element(By.TAG_NAME, "button")
confirm_btn.click()
logger.info("已点击弹窗确认按钮。")
time.sleep(1)
elif action == "answer_and_close":
# 处理测验:这里需要更复杂的逻辑,比如随机选择答案
self._handle_quiz(popup)
except NoSuchElementException:
continue # 没找到这个弹窗,检查下一个
except Exception as e:
logger.error(f"处理弹窗 {selector} 时出错: {e}")
def _handle_quiz(self, quiz_popup_element):
"""
处理随堂测验(简单版:随机选择第一个选项并提交)
实际应用可能需要更智能的逻辑,比如读取题目
"""
logger.info("正在处理随堂测验...")
try:
# 假设每个选项是一个 radio input 或 checkbox
options = quiz_popup_element.find_elements(By.CSS_SELECTOR, "input[type='radio'], input[type='checkbox']")
if options:
# 随机选择一个(或者选第一个)
import random
option_to_select = random.choice(options) if len(options) > 1 else options[0]
# 如果元素不可点击,可能需要通过JavaScript点击
self.driver.execute_script("arguments[0].click();", option_to_select)
logger.info("已选择一个答案。")
time.sleep(1)
# 查找并点击提交按钮
submit_btn = quiz_popup_element.find_element(By.CSS_SELECTOR, "button.submit, input[type='submit']")
submit_btn.click()
logger.info("已提交测验答案。")
time.sleep(2) # 等待结果提交
except Exception as e:
logger.error(f"处理测验失败: {e}")
# 如果处理失败,可以尝试直接关闭弹窗(如果有关闭按钮)
try:
close_btn = quiz_popup_element.find_element(By.CLASS_NAME, "close")
close_btn.click()
except:
pass
避坑指南 :
- 弹窗处理顺序 :将最常见的弹窗选择器放在列表前面,提高检测效率。
- 非阻塞检测 :使用
find_element而不是WebDriverWait来检测弹窗,因为弹窗不是一直存在。find_element找不到会立刻抛出异常,我们可以捕获这个异常并继续。 - 测验处理 :自动答题有风险。简单的随机选择可能答错,导致需要重看。更稳妥的方法是:1) 记录下题目和正确答案,建立本地题库,下次遇到直接选;2) 如果允许,先暂停脚本,手动答题后再继续。这涉及到更复杂的交互,需要权衡自动化程度与稳定性。
5. 完整脚本整合与优化策略
5.1 主循环与脚本入口
将上述所有模块整合起来,形成一个可以完整运行的脚本。
def run(self, username, password, login_url, course_selector):
"""
主运行流程
"""
try:
# 1. 登录
self.login(username, password, login_url)
# 2. 进入课程
self.navigate_to_course(course_selector)
# 3. 获取章节列表
# 这里的章节容器选择器需要你根据实际页面确定
chapters = self.get_chapter_list("div.chapter-list-container")
if not chapters:
logger.error("未找到任何章节,脚本退出。")
return
# 4. 遍历所有章节/视频
for idx, chapter in enumerate(chapters):
logger.info(f"开始处理第 {idx + 1}/{len(chapters)} 个章节。")
# 检查是否已完成(避免重复学习)
if self.is_chapter_completed(chapter):
logger.info("该章节已完成,跳过。")
continue
# 处理视频播放
self.play_video_and_monitor(chapter)
# 在处理每个视频后,随机等待一段时间,模拟人类行为
wait_time = random.uniform(5, 15)
logger.info(f"随机等待 {wait_time:.1f} 秒...")
time.sleep(wait_time)
logger.info("所有章节处理完毕!")
except Exception as e:
logger.error(f"主流程运行失败: {e}")
self.driver.save_screenshot('main_flow_error.png')
finally:
# 无论成功与否,最后都关闭浏览器
input("按回车键结束程序并关闭浏览器...")
self.driver.quit()
def is_chapter_completed(self, chapter_element):
"""
检查章节是否已标记为完成
:return: True 如果已完成
"""
try:
# 查找表示“已完成”的标识,比如一个勾选图标、特定的类名或文本
completed_marker = chapter_element.find_element(By.CSS_SELECTOR, ".completed, .finished, .checked")
# 或者检查文本内容
# if "已完成" in chapter_element.text:
# return True
return True
except NoSuchElementException:
return False
if __name__ == "__main__":
# 配置你的信息
YOUR_USERNAME = "你的账号"
YOUR_PASSWORD = "你的密码"
LOGIN_URL = "经世优学平台的登录页面URL"
COURSE_SELECTOR = "CSS选择器,用于定位你要学习的课程链接"
# 创建实例并运行
# headless=True 表示无头模式(不显示浏览器界面)
bot = AutoCourse(headless=False) # 调试时建议设为False,可以看到浏览器操作
bot.run(YOUR_USERNAME, YOUR_PASSWORD, LOGIN_URL, COURSE_SELECTOR)
5.2 对抗反自动化策略与稳健性提升
平台不会坐视不管,它们会升级反自动化策略。我们的脚本也需要不断进化。
-
特征隐藏 :我们已经在初始化选项中添加了部分参数来隐藏 WebDriver 特征。更进一步的,可以尝试:
# 在初始化后执行JavaScript,覆盖navigator.webdriver属性 self.driver.execute_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})") -
行为模拟 :
- 随机延迟 :在所有关键操作(点击、输入)前后加入随机等待时间,避免固定节奏。
- 鼠标移动 :使用
ActionChains模拟更自然的鼠标移动轨迹,而不是直接从A点跳到B点。
from selenium.webdriver.common.action_chains import ActionChains element = driver.find_element(...) actions = ActionChains(driver) actions.move_to_element(element).perform() time.sleep(random.uniform(0.1, 0.5)) actions.click().perform()- 页面滚动 :偶尔滚动一下页面,模拟阅读行为。
-
断点续学与状态保存 :脚本可能因为网络、弹窗或其他异常中断。一个健壮的脚本应该能记录进度,下次从中断处继续。可以将已完成的章节ID或标题保存到一个本地文件(如JSON或TXT),每次运行前先读取这个文件,跳过已完成的章节。
-
多账号与调度 :如果你需要管理多个账号,可以将账号密码保存在一个配置文件中,用循环遍历。更高级的,可以结合任务调度器(如 Windows 的 Task Scheduler 或 Linux 的 cron)在指定时间自动运行脚本。
-
日志与通知 :完善的日志 (
logging模块) 至关重要。你还可以集成邮件或消息推送(如 Server酱、PushPlus),在脚本完成、出错或遇到无法处理的弹窗时通知你。
6. 常见问题排查与调试技巧
即使代码写得再仔细,运行时也总会遇到各种问题。这里记录了我踩过的一些坑和解决方法。
6.1 元素定位失败(NoSuchElementException)
这是最常见的问题,意味着 Selenium 在页面中找不到你指定的元素。
可能原因及解决方案:
| 问题原因 | 排查方法 | 解决方案 |
|---|---|---|
| 页面未加载完成 | 查看截图,元素是否在页面上? | 在操作前增加等待。使用 WebDriverWait 配合 EC.presence_of_element_located 或 EC.element_to_be_clickable 。 |
| iframe 嵌套 | 检查目标元素是否在 <iframe> 标签内。 |
使用 driver.switch_to.frame(frame_element_or_id) 切换到对应的 iframe 后再定位元素。操作完后用 driver.switch_to.default_content() 切回。 |
| 元素属性动态变化 | 检查元素的 id 、 class 是否每次加载都不同。 |
使用更稳定的定位方式,如 XPath 基于文本内容 ( //button[contains(text(),‘登录’)] ) 或相对路径。 |
| 页面结构已更新 | 对比之前保存的页面HTML和现在的页面。 | 更新你的选择器。这是维护自动化脚本的常态。 |
| 选择器写错了 | 在浏览器控制台用 document.querySelector(‘你的选择器’) 测试。 |
修正 CSS 选择器或 XPath 语法。 |
调试技巧 :当定位失败时,立刻截图 ( driver.save_screenshot(‘error.png’) ) 并打印当前页面的源代码 ( driver.page_source )。这能帮你直观地看到脚本“眼中”的页面状态。
6.2 脚本被检测到
如果登录失败,或者进行某些操作时被重定向到验证页面,可能是触发了平台的反爬机制。
应对策略:
- 使用更真实的浏览器环境 :尝试去掉
headless模式(无头模式更容易被检测)。或者使用undetected-chromedriver这类专门修改过的驱动。 - 降低操作频率 :大幅增加操作间的随机等待时间,模拟真人犹豫和阅读时间。
- 模拟真人浏览 :在播放视频的间隙,随机执行一些浏览操作,如滚动页面、移动鼠标到不相关区域等。
- Cookie 复用 :先手动登录一次,用
pickle库保存登录后的 Cookies。下次脚本启动时,先加载 Cookies 再访问页面,可能绕过登录检测。但需注意 Cookies 有效期。
6.3 视频监控失灵
视频播放监控是最容易出错的环节,因为播放器实现五花八门。
排查清单:
- 进度元素找不到 :播放器的当前时间、总时间可能不是通过标准HTML元素显示的,而是画在 Canvas 里或通过复杂的 JavaScript 控制。这时, 基于时间的等待 或 检测完成标志 是更可靠的备选方案。
- 视频卡住但进度在变 :有些播放器在缓冲时,当前时间数字会卡住,但视频实际已暂停。除了检测进度,还可以尝试定期(比如每分钟)检查视频的
paused属性(通过JavaScript执行document.querySelector(‘video’).paused),如果为true则点击播放按钮。 - 弹窗打断监控 :确保你的
handle_popups_during_playback函数在监控循环中被高频调用(比如每次检查进度时都调用一次)。
6.4 网络不稳定与重试机制
网络波动可能导致页面加载失败或请求超时。
增强稳健性:
from selenium.common.exceptions import WebDriverException
def robust_find_element(driver, by, selector, retries=3):
"""带有重试机制的元素查找"""
for i in range(retries):
try:
element = driver.find_element(by, selector)
return element
except NoSuchElementException:
if i == retries - 1:
raise
logger.warning(f"元素 {selector} 未找到,第 {i+1} 次重试...")
time.sleep(2 ** i) # 指数退避等待
except WebDriverException as e:
logger.warning(f"查找元素时发生网络错误: {e},第 {i+1} 次重试...")
time.sleep(2 ** i)
return None
将核心的 find_element 和 click 操作包裹在重试逻辑中,可以显著提升脚本在恶劣网络环境下的生存能力。
最后,记住自动化挂网课的目的是 提升效率,解放时间用于更有价值的学习或工作 。这个脚本是一个起点,你需要根据目标平台的具体情况不断调整和优化。从简单的等待播放,到处理弹窗,再到对抗检测,每一步的深入都让你对 Web 自动化的理解更深一层。在实际使用中,请务必遵守平台的使用条款,合理使用自动化工具。
更多推荐
所有评论(0)