1. 项目概述当“刷课”遇上自动化如果你还在为那些冗长、枯燥的在线课程特别是那些强制观看视频后弹出的、答案千篇一律的课后习题而烦恼那么今天分享的这个Python脚本可能就是你的“解放双手”利器。我把它称为“课程伴侣”本质上是一个基于浏览器自动化工具Selenium开发的自动答题脚本。它的核心目标非常明确模拟真人操作自动完成网页端课程视频的观看或跳过以及后续测验的自动作答将你从重复性劳动中彻底解脱出来。这个项目的诞生源于一个非常普遍且具体的需求——各类在线学习平台上的课程任务。这些任务往往设计得机械且耗时其考核重点并非真正的知识掌握而是“完成度”。手动操作不仅效率低下还容易因枯燥而出错。因此一个能够精准识别题目、分析选项并提交答案的自动化工具其价值不言而喻。我通过整合Selenium的网页操控能力、针对性的元素定位策略以及灵活的逻辑判断最终实现了一个在特定场景下正确率高达99%的解决方案。它并非破解或攻击工具而是将我们从无意义的流程性工作中解放出来把宝贵的时间投入到真正需要思考和创造的学习内容上去。2. 核心思路与技术选型解析2.1 为什么是Selenium而不是Requests或Pyppeteer实现网页自动化常见的库还有Requests用于HTTP请求和Pyppeteer一个Python版的Puppeteer。选择Selenium作为核心是基于目标场景的深思熟虑。首先我们的操作对象是高度动态化的现代Web应用。在线课程平台的前端大量使用JavaScript来渲染页面、加载视频播放器、动态生成题目和选项。使用Requests库直接发送HTTP请求虽然效率高但需要逆向分析复杂的API接口和参数对于不同平台需要定制不同的解析逻辑通用性差且维护成本极高。而Selenium直接驱动一个真实的浏览器如Chrome, Edge, Firefox能够完整地执行页面中的所有JavaScript代码看到的就是用户肉眼看到的最终页面这完美契合了我们“模拟真人操作”的核心需求。其次与Pyppeteer相比Selenium拥有更悠久的历史、更庞大的社区和更丰富的资料。这意味着在遇到诸如“iframe嵌套”、“动态元素加载”、“弹窗处理”等具体问题时更容易找到成熟的解决方案和代码片段。此外Selenium支持多种浏览器对于需要在不同环境下比如某些平台对特定浏览器有优化运行脚本的情况切换成本更低。虽然Pyppeteer在性能和控制粒度上可能有优势但在此类对性能不敏感、更追求稳定和易用的自动化任务中Selenium的生态优势是决定性的。注意使用任何自动化工具访问网站都必须严格遵守该网站的robots.txt协议和服务条款。本脚本仅用于学习自动化技术原理请勿将其用于任何干扰网站正常服务、侵犯他人权益或违反相关规定的用途。合理使用技术尊重平台规则。2.2 脚本的宏观工作流程设计一个健壮的自动答题脚本其工作流必须是线性的、容错的和可配置的。不能简单地“找到按钮就点击”必须考虑到网络延迟、元素加载时间、意外弹窗等各种现实情况。我设计的核心流程如下初始化与登录启动浏览器加载目标课程网站自动填充用户名和密码完成登录。这里的关键是处理登录状态保持如Cookie和可能的验证码对于复杂验证码本脚本不涉及破解可能需要手动干预或采用其他合规方案。导航与课程定位登录后自动跳转到“我的课程”或指定课程链接页面。通过课程名称或ID精准定位到需要完成的目标课程单元。章节与任务遍历进入课程后解析课程大纲依次遍历每个章节、每个视频或图文任务。这里需要处理复杂的页面DOM结构准确找到“任务列表”。任务状态判断与执行对于每个任务首先判断其状态“未开始”、“进行中”、“已完成”。仅对“未开始”或“进行中”的任务执行自动化操作。视频任务处理如果是视频任务核心是获取视频时长然后等待足够的时间或寻找并点击“跳过”按钮模拟观看完成。同时监测“播放”按钮状态确保视频确实开始播放。测验任务处理核心如果是测验答题任务进入答题页面。这里是脚本的“大脑”题目与选项提取稳定地定位到题目题干和所有选项的HTML元素并获取其文本内容。答案匹配与决策将题目和选项文本与本地预置的“题库”一个结构化的数据文件如JSON或SQLite进行匹配。根据匹配结果决定是单选、多选还是判断题并模拟点击对应选项。容错与降级策略如果题库中没有匹配到答案则触发降级策略例如记录该题目并跳过或根据选项文本中的关键词如“正确”、“错误”、“是”、“否”进行简单逻辑判断甚至可以选择一个固定选项如第一项以确保流程继续。提交与状态同步答题完成后自动点击“提交”或“下一题”按钮。提交后验证是否出现“提交成功”的提示并更新任务状态。循环与结束一个章节的所有任务完成后自动返回章节列表继续下一个章节直至所有课程任务标记为“已完成”。最后脚本优雅地关闭浏览器并生成一份简单的执行报告如完成了多少任务跳过了多少无答案的题目等。这个流程中的每一步都需要加入显式等待WebDriverWait来应对网络波动加入异常捕获try...except来处理元素找不到等意外情况确保脚本能够长时间稳定运行而不崩溃。3. 核心模块拆解与实现细节3.1 环境搭建与Selenium配置工欲善其事必先利其器。一个稳定的环境是脚本成功运行的第一步。我强烈推荐使用virtualenv或conda创建独立的Python环境避免包版本冲突。1. 安装核心库pip install selenium这是最核心的库。同时建议安装webdriver-manager它可以自动下载和管理不同浏览器的驱动省去手动配置的麻烦。pip install webdriver-manager2. 浏览器驱动与“扩展程序”问题这是新手最容易踩坑的地方。以Edge浏览器为例如果你遇到类似“python selenium要在edge启用一个扩展程序”或“已经从microsoft获取到扩展”的提示或错误通常是因为Selenium使用的浏览器用户配置文件问题。解决方案是使用无痕模式或指定明确的用户数据目录并禁用扩展。以下是通过webdriver-manager和自定义选项启动Edge的推荐方式from selenium import webdriver from selenium.webdriver.edge.service import Service as EdgeService from webdriver_manager.microsoft import EdgeChromiumDriverManager from selenium.webdriver.edge.options import Options # 配置浏览器选项 edge_options Options() # 关键配置1使用无痕模式避免现有用户配置和扩展的干扰 edge_options.add_argument(--inprivate) # 关键配置2禁用扩展 edge_options.add_argument(--disable-extensions) # 其他优化配置 edge_options.add_argument(--disable-blink-featuresAutomationControlled) # 隐藏自动化特征 edge_options.add_experimental_option(excludeSwitches, [enable-automation]) edge_options.add_experimental_option(useAutomationExtension, False) # 自动下载并配置Edge驱动 service EdgeService(EdgeChromiumDriverManager().install()) driver webdriver.Edge(serviceservice, optionsedge_options) # 执行后续操作... driver.get(https://your-course-site.com)对于Chrome配置类似使用ChromeDriverManager和ChromeOptions即可。这样配置后就能以一个“干净”的浏览器环境启动避免了很多因本地配置导致的诡异问题。3.2 页面元素定位与智能等待策略定位元素是Selenium自动化的一切基础。我们的目标是写出健壮而非脆弱的定位代码。1. 首选定位策略ID、Name最稳定但现代网页中已不常见。CSS Selector 和 XPath是主力军。CSS Selector通常性能更好语法更简洁XPath功能更强大可以遍历DOM树定位没有明显特征的元素。示例CSSdriver.find_element(By.CSS_SELECTOR, “button.submit-btn.primary”)示例XPathdriver.find_element(By.XPATH, “//div[class‘question’]//p[contains(text(), ‘以下哪项’)]”)绝对避免使用依赖于页面结构顺序的索引定位如:nth-child(3)除非万不得已因为页面结构微调就会导致脚本失效。2. 智能等待告别sleep使用time.sleep(10)是极不推荐的它固定等待无论元素是否已加载都浪费了时间。Selenium提供了两种优秀的等待方式隐式等待driver.implicitly_wait(10)。设置一个全局的超时时间在查找任何元素时如果元素没有立即出现WebDriver会轮询DOM直到找到它或超时。它是一劳永逸的但对某些复杂的动态加载可能不够用。显式等待这是实现高稳定性的关键。它允许你为某个特定条件设置等待。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By # 等待“提交按钮”可被点击最多等10秒 submit_button WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, “submitAnswer”)) ) submit_button.click()常用的条件还有presence_of_element_located元素出现在DOM、visibility_of_element_located元素可见等。在关键操作前如点击按钮、获取题目文本使用显式等待能极大提升脚本的容错能力。3.3 题库构建与答案匹配引擎这是实现“99%正确率”的灵魂所在。题库不能是简单的文本堆砌需要设计成易于查询和扩展的结构。1. 题库数据结构设计我推荐使用JSON格式因为它易于人类阅读和修改也方便Python直接解析。{ “课程名称A”: { “章节1”: [ { “question”: “Python中用于定义函数的关键字是什么”, “type”: “single”, // “single”, “multiple”, “judge” “answer”: [“def”], “options”: [“function”, “def”, “define”, “func”] }, { “question”: “下列哪些是Python的基本数据类型多选”, “type”: “multiple”, “answer”: [“int”, “str”, “list”], “options”: [“int”, “str”, “dict”, “list”, “class”] } ], “章节2”: [...] }, “课程名称B”: {...} }2. 答案匹配策略直接进行字符串完全匹配 (if question_text stored_question) 是非常脆弱的因为网页上的题目文本可能包含多余的空格、换行符或标点。必须进行模糊匹配。我使用difflib库中的SequenceMatcher或get_close_matches函数它能够计算文本相似度。import difflib def find_best_match(current_question, question_list): “”“在题库列表中寻找与当前问题最相似的问题。”“” # question_list 是从题库中加载的、同一章节下的所有题目字典列表 best_ratio 0 best_match None for q in question_list: ratio difflib.SequenceMatcher(None, current_question, q[“question”]).ratio() if ratio best_ratio: best_ratio ratio best_match q # 设置一个相似度阈值比如0.9高于这个阈值才认为是同一道题 if best_match and best_ratio 0.9: return best_match else: return None匹配到题目后根据type字段决定如何操作。对于单选题和判断题直接点击匹配到的答案选项对于多选题需要遍历点击所有在answer数组中的选项。3. 题库的维护与更新初始题库可以是空的。脚本可以设计一个“学习模式”当遇到题库中没有的题目时不是盲目选择而是将题目、选项和正确答案如果之后通过其他方式获知记录下来追加到本地题库文件中。这样脚本会随着使用次数的增加而变得越来越“聪明”正确率自然趋近100%。这就是“99%正确率”的可持续实现路径。4. 完整脚本流程与关键代码实现让我们将上述模块串联起来看看核心循环部分的代码骨架。请注意以下代码是概念性示例你需要根据目标网站的具体HTML结构进行调整。from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import json import difflib import time class AutoCourseBot: def __init__(self, driver, course_data): self.driver driver self.course_db course_data # 加载到内存的题库字典 self.wait WebDriverWait(driver, 15) def process_chapter(self, chapter_link, chapter_name): “”“处理一个章节”“” self.driver.get(chapter_link) # 1. 获取本章节所有任务列表 task_items self.wait.until( EC.presence_of_all_elements_located((By.CSS_SELECTOR, “.task-item”)) ) for task in task_items: # 2. 判断任务类型和状态 task_type task.find_element(By.CSS_SELECTOR, “.type-icon”).get_attribute(“title”) status task.find_element(By.CSS_SELECTOR, “.status”).text if “已完成” in status: print(f“任务已跳过状态{status}”) continue # 进入任务 task.find_element(By.CSS_SELECTOR, “.title a”).click() # 3. 根据类型处理 if “视频” in task_type: self._handle_video_task() elif “测验” in task_type or “作业” in task_type: self._handle_quiz_task(chapter_name) # 处理完返回章节列表 self.driver.back() time.sleep(2) # 简单等待页面跳转 def _handle_video_task(self): “”“处理视频任务等待播放完毕或寻找跳过按钮”“” try: # 尝试找“跳过”按钮 skip_btn self.wait.until( EC.element_to_be_clickable((By.XPATH, “//button[contains(text(), ‘跳过’)]”)) ) skip_btn.click() print(“视频任务已跳过”) except: # 没有跳过按钮则获取视频时长并等待 duration_str self.wait.until( EC.presence_of_element_located((By.CLASS_NAME, “duration”)) ).text # 将“12:34”转换成秒 minutes, seconds map(int, duration_str.split(‘:’)) total_seconds minutes * 60 seconds print(f“视频任务开始播放需等待{total_seconds}秒”) # 确保视频开始播放 play_btn self.driver.find_element(By.CSS_SELECTOR, “.play-button”) if “播放” in play_btn.text: play_btn.click() time.sleep(total_seconds 5) # 多等几秒确保完成 # 视频结束后通常有“下一步”或“完成”按钮 next_btn self.wait.until( EC.element_to_be_clickable((By.XPATH, “//button[contains(text(), ‘下一’) or contains(text(), ‘完成’)]”)) ) next_btn.click() def _handle_quiz_task(self, chapter_name): “”“处理测验任务核心答题逻辑”“” print(f“进入测验任务章节{chapter_name}”) # 获取当前章节的题库 chapter_questions self.course_db.get(“当前课程名”, {}).get(chapter_name, []) question_index 0 while True: question_index 1 print(f“正在处理第 {question_index} 题...”) # 1. 提取题目文本 (需要根据实际网页结构调整选择器) question_element self.wait.until( EC.presence_of_element_located((By.CSS_SELECTOR, “.question-stem”)) ) question_text question_element.text.strip() # 2. 提取选项元素和文本 option_elements self.driver.find_elements(By.CSS_SELECTOR, “.option-item”) option_texts [opt.text.strip() for opt in option_elements] # 3. 从题库中匹配 matched_q self._find_question_in_db(question_text, chapter_questions) if matched_q: print(f“题库匹配成功答案{matched_q[‘answer’]}”) # 4. 根据匹配结果点击选项 self._select_answer(matched_q, option_elements, option_texts) else: print(“题库未匹配执行降级策略...”) # 降级策略例如记录到日志文件供后续补充 with open(“unanswered_questions.log”, “a”, encoding“utf-8”) as f: f.write(f“[{chapter_name}] {question_text}\n 选项{option_texts}\n\n”) # 或者尝试智能猜测例如判断题找“正确”“错误”关键词 self._guess_answer(question_text, option_elements, option_texts) # 5. 点击“下一题”或“提交” try: next_btn self.driver.find_element(By.XPATH, “//button[contains(text(), ‘下一题’)]”) next_btn.click() time.sleep(1) # 等待下一题加载 except: # 没有“下一题”按钮说明是最后一题点击提交 submit_btn self.wait.until( EC.element_to_be_clickable((By.XPATH, “//button[contains(text(), ‘提交’)]”)) ) submit_btn.click() # 处理提交后的确认弹窗等 confirm_btn self.wait.until( EC.element_to_be_clickable((By.XPATH, “//button[contains(text(), ‘确认’)]”)) ) confirm_btn.click() print(“测验提交完成”) break # 退出答题循环 def _find_question_in_db(self, current_q, q_list): “”“模糊匹配题目”“” for q in q_list: ratio difflib.SequenceMatcher(None, current_q, q[“question”]).ratio() if ratio 0.9: # 相似度阈值 return q return None def _select_answer(self, matched_q, option_elements, option_texts): “”“根据题库答案选择选项”“” correct_answers matched_q[“answer”] # 是个列表 q_type matched_q[“type”] for idx, opt_text in enumerate(option_texts): # 对于单选题和判断题答案列表通常只有一个元素 # 对于多选题需要遍历所有选项 if q_type “single” or q_type “judge”: if opt_text in correct_answers: option_elements[idx].click() break elif q_type “multiple”: if opt_text in correct_answers: option_elements[idx].click() # 主程序入口 if __name__ “__main__”: # 1. 初始化浏览器驱动 (使用前面推荐的配置) # 2. 登录网站 (此处省略登录逻辑需自行实现) # 3. 加载题库 with open(“question_bank.json”, “r”, encoding“utf-8”) as f: course_data json.load(f) # 4. 实例化机器人 bot AutoCourseBot(driver, course_data) # 5. 导航到课程主页获取章节链接列表 (需根据实际网站实现) chapter_links [...] # 假设这是获取到的章节链接和名称列表 for link, name in chapter_links: bot.process_chapter(link, name) # 6. 完成后关闭浏览器 driver.quit()5. 实战避坑指南与进阶优化在实际部署和运行过程中你会遇到各种各样的问题。下面是我踩过坑后总结出的关键点。5.1 常见问题与排查技巧问题现象可能原因排查与解决方案NoSuchElementException(元素找不到)1. 页面未加载完成。2. 元素在iframe内。3. 元素是动态生成的选择器不对。4. 页面结构已更新。1.增加显式等待使用EC.presence_of_element_located等条件。2. 使用driver.switch_to.frame(frame_element)切换到对应的iframe。3. 使用浏览器开发者工具F12的Inspector仔细检查元素尝试更稳定的CSS或XPath。4. 更新你的元素定位代码。ElementClickInterceptedException(元素点击被拦截)元素被其他元素如弹窗、遮罩层覆盖。1. 等待覆盖层消失。2. 使用JavaScript直接点击driver.execute_script(“arguments[0].click();”, element)。脚本被网站检测为机器人浏览器指纹、WebDriver属性暴露。1. 添加options.add_argument(“--disable-blink-featuresAutomationControlled”)等选项。2. 使用undetected-chromedriver等更高级的库需谨慎评估合规性。3.最重要的放慢操作速度在关键步骤间加入随机延时如time.sleep(random.uniform(1, 3))模拟人类操作的不确定性。题库匹配率低1. 题目文本包含可变部分如日期、数字。2. 相似度阈值设置不合理。1. 在匹配前对题目文本进行清洗移除数字、特殊符号只保留核心语义词汇。2. 调整模糊匹配的阈值并通过日志输出匹配率和匹配到的题目进行人工校准。多窗口/标签页处理混乱点击链接可能在新窗口打开。使用driver.window_handles获取所有窗口句柄并通过driver.switch_to.window(handle)进行切换。操作完毕后记得关闭多余窗口并切回主窗口。验证码网站增加了验证码防护。这是自动化脚本的“天敌”。合规的解决方案包括1. 设计脚本在遇到验证码时暂停并提示用户手动输入。2. 研究平台是否提供API接口通过合法途径获取数据。切勿尝试破解验证码这通常违反服务条款且涉及复杂技术。5.2 提升脚本鲁棒性与效率的进阶技巧结构化日志与错误恢复不要只用print。使用Python的logging模块将运行信息、遇到的题目、发生的错误分级记录到文件。甚至可以设计断点续做功能当脚本意外中断时能从上次完成的章节或题目继续而不是重头开始。配置化将浏览器类型、登录账号、课程链接、等待超时时间、相似度阈值等所有可变参数提取到外部的配置文件如config.yaml或config.ini中。这样无需修改代码就能适应不同环境。使用Page Object Model (POM) 设计模式如果脚本非常复杂或需要维护多个不同平台的版本强烈推荐使用POM。将每个页面如登录页、课程主页、答题页封装成一个类页面的元素定位和基本操作如输入、点击作为类的方法。这样能使代码结构清晰易于维护和复用。异步操作考虑对于纯粹的顺序任务Selenium的同步操作足够。但如果需要同时监控多个任务状态可以考虑结合asyncio和threading但复杂度会急剧上升需谨慎评估。打包与分发使用PyInstaller或cx_Freeze将脚本和Python环境打包成一个独立的.exe可执行文件方便在没有Python环境的Windows电脑上运行。记得将题库文件、配置文件等资源一起打包并在代码中处理好资源路径的问题。最后我必须再次强调技术的边界与责任。这个脚本是一个绝佳的Python和Selenium学习项目它涵盖了网络请求、数据解析、模糊匹配、异常处理、设计模式等多个知识点。但请务必将其用于个人学习和技术研究或在明确允许自动化的场景下使用。尊重平台规则和他人劳动成果让技术成为提高效率的工具而非制造麻烦的源头。在编写和运行这类脚本时保持一个合理的请求间隔避免对服务器造成不必要的压力这是每一个技术爱好者应有的素养。