
“用 Selenium 自动化抓取在线学习平台作业答案”——说实话我最早看到类似的项目需求时第一反应是这东西没什么技术含量用 requests 模拟一下请求、解析一下 HTML 不就行了但真正上手之后才发现这类在线学习平台的前端早就不是十年前那种“服务端渲染完直接给你 HTML”的玩法了。页面全是异步加载、动态渲染甚至按钮的点击状态都藏在某个 JavaScript 变量里。这时候 Selenium 的价值才真正体现它不跟你玩协议、不猜接口直接用真人浏览器视角完成“看到——点击——读取”的全流程。这篇文章我不打算教你如何去拿某个平台的答案库那既违反平台规则也不尊重课程设计者的劳动。我会把这件事拆解成一个更干净的版本如何用 Selenium 自动化完成一套“练习题页面数据采集与状态记录”的流程。这套流程覆盖登录、页面跳转、DOM 定位、显式等待、iframe 切换、数据落盘等核心技术点。你把这套能力吃透之后用到自动化测试、数据采集、流程巡检都完全成立。1. 为什么会盯上 Selenium项目动机与技术选型1.1 这类需求背后的真实诉求“抓取作业答案”这类标题本质上反映的是一种需求用脚本代替手工重复操作。用户真正需要的往往不是题目答案本身而是“我想用程序把自己的账号里那几十套练习题的题干、选项、我的提交记录、判分结果统一导出到 Excel 里方便做知识复习整理”。这种批量导出的需求在考研刷题、考证备考、内部培训考核的场景里非常常见。答案是什么不重要重要的是你要顺手掌握“如何稳定地把网页上反复出现的数据结构抽出来”这套方法论。我见过太多人一开始只是为了抄答案后来做着做着就入了自动化测试的门玩起了测试框架和 CI 集成。所以别把这标题当歪门邪道把它当个练习项目就好。1.2 为什么 requests BeautifulSoup 搞不定这类页面拿某在线学习平台的课程练习页面举例你用 requests 请求练习页 URL拿回来的 HTML 里根本找不到题干和选项。原因很简单这些内容不是写在初始响应里的而是页面在 onload 之后通过 XHR 请求 API 接口拿 JSON 数据再交给前端框架渲染出来的。你打开浏览器开发者工具切到 Network 面板刷新页面能看到十几个请求其中某个 .json 或带 token 参数的接口才是真正返回题目数据的。理论上你可以逆向这个接口用 requests 直接调它但问题随之而来token 怎么生成要不要做签名校验请求头里的防爬参数如何构造返回的 JSON 结构是否会低频调整一旦接口做了调整你的脚本立刻失效又得花时间抓包、比对、改代码。Selenium 不存在这个问题。它操作的是真实浏览器所有请求、cookie、渲染、事件绑定都是浏览器自己完成的你只需要关心“人眼看到的东西”——也就是最终渲染出来的 DOM 结构。数据字段变了改一下 CSS 选择器就行成本低得多。1.3 Selenium 的定位与能力边界Selenium 不是抓包工具也不是接口调试工具它是“浏览器自动化框架”。它通过 WebDriver 协议和浏览器内核沟通模拟真人操作打开网页执行跳转切换标签页填写表单点击按钮处理弹窗执行 JavaScript提取元素属性等待某个条件满足后再继续它的能力边界也很清晰凡是真人能在浏览器里做的它基本都能做凡是得靠协议逆向才能做的比如直接伪造接口数据它不一定擅长也不建议去做。所以这个项目的正确玩法是把浏览器当成你的无头小助手让它按固定的流程走一遍把页面上该读取的数据稳定抓下来。这样既不需要逆向签名算法也能最大程度贴合平台前端的变化节奏。2. 环境准备别在第一步就栽跟头2.1 安装 Selenium 与浏览器驱动环境是这类项目里最容易让人劝退的关卡。先装 Python 的 selenium 库这一步几乎不会出问题pip install selenium真正麻烦的是浏览器驱动。Chrome 和 ChromeDriver 之间有着严格的版本对应关系版本不匹配启动浏览器时直接抛 SessionNotCreatedException。手动下载驱动然后配环境变量的老办法太累人了我现在都推荐用 webdriver-managerpip install webdriver-manager用起来非常顺滑from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager driver webdriver.Chrome( serviceService(ChromeDriverManager().install()) ) driver.get(https://example.com)首次运行时它会自动下载匹配的驱动后续走本地缓存。这能省掉八成环境问题。2.2 三个最容易忽略的环境问题第一Chrome 的自动更新会静默升级浏览器版本导致之前匹配好的驱动突然失灵。我建议要么关掉 Chrome 的自动更新要么每次重跑脚本时让它先检测一次驱动版本。第二Linux 服务器上跑这个项目通常缺各种系统依赖库比如 libnss3、libgconf-2-4 之类的。启动时报缺少共享库的错误时别慌缺什么yum或apt装什么就行。第三无头模式headless在中文页面上的字体渲染可能导致元素宽度跟有头模式不一致个别 CSS 选择器在无头模式下会选不到元素或者元素被判定为不可见。调试时先用有头模式脚本稳定后再切无头。注意无头模式加上禁用 GPU 这两个参数并非必须但如果你的环境是 Linux 服务器最好把这两项都设置好不然很容易出现诡异的启动失败。2.3 浏览器被识别为自动化工具的应对现在的平台基本都会在前端挂检测脚本检查 window.navigator.webdriver 是否为 true。一旦检测到行为正常的账号也可能被要求额外验证。常规手段是在启动参数里加排除项options webdriver.ChromeOptions() options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) options.add_argument(--disable-blink-featuresAutomationControlled)需要说明这些手段不会让你突破底线去做什么违规的事情它们的作用只是让你的脚本在合规使用场景下减少被平台无差别风控的干扰。如果你要在生产环境长期跑更稳妥的方案是用浏览器的用户数据目录登录一次把登录状态持久化避免频繁触发登录验证options.add_argument(--user-data-dir/path/to/profile)这样手动登录一次之后后续启动都带着这个身份态。要注意控制请求节奏Selenium 本身已经够“拟人”了没必要再加代理池之类的骚操作保持低调才是长期稳定运行的前提。3. 核心机制定位、等待、执行脚本3.1 DOM 定位不要迷信最简写法很多初学者的第一直觉是 driver.find_element(By.CLASS_NAME, “xxx”)结果经常因为 class 带有动态变化或复合名称而直接报错。定位元素要遵循一个优先级逻辑优先用 idid 是相对稳定的锚点其次用 CSS 选择器借由结构找要素再次用 XPath解决复杂结构和文本匹配场景最后才用 class 名称重点是选那种业务自定义的、不随渲染变化的静态 class还有个建议在写定位表达式之前先在浏览器开发者工具的 Console 里验证一下 document.querySelector(你的选择器) 能不能选到唯一目标。这一步能省很多来回调试的时间。3.2 显式等待与隐式等待的取舍Selenium 等待机制分三类强制等待、隐式等待、显式等待。强制等待就是 time.sleep(3)稳定但笨拙页面快了浪费时间页面慢了照样崩。隐式等待是 driver.implicitly_wait(10)设置之后所有 find 操作都会在元素未出现时进行轮询但它管不了“元素已出现但内容未更新”的情况。真正要养成习惯用的是显式等待from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, 15) question_block wait.until( EC.presence_of_element_located((By.CSS_SELECTOR, div.question-item)) )显式等待的优雅之处在于它明确表达“我等的就是这个条件”可读性强且不受全局设置影响。我个人的习惯是不设隐式等待全程用显式等待每个关键节点明确等它该等的东西比如元素可见、元素可点击、某个文本出现。3.3 execute_script 的妙用Selenium 原生 API 覆盖了绝大多数点击、输入场景但总有些交互是它搞不定的比如页面里某个按钮绑定了一堆自定义鼠标事件又比较傲娇。这时候别硬跟它较劲直接上 JavaScript。driver.execute_script(arguments[0].click();, button_element)同理读取元素不可见文本、获取元素的滚动高度、强制设置为可点击状态这些场景都适合用 execute_script 解决。但要记住JavaScript 是绕过了事件模型的如果某个按钮的点击回调依赖 mousedown、mouseup 事件的完整顺序直接用 click() 触发可能不生效这时候就得模拟完整的事件序列。所以我的原则是先试原生方法原生不行再上 JS别上来就极端。4. 实操把练习题页面变成结构化数据下面这一整套流程我以某个在线学习平台的课程练习模块作为演示场景把“抓取作业答案”这个敏感需求替换成“自动化整理练习题集”效果一样技术细节完全通用。4.1 从登录启动到页面到达平台都有扫码登录或账密登录。账密登录这种形式对自动化的干扰最少但要注意登录接口可能在第二重验证上做文章。我的建议是把登录状态持久化用 user-data-dir 那种方案只用脚本完成一次完整的登录流程把关键 cookies 保存下来。代码骨架大致长这样import pickle 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 options webdriver.ChromeOptions() options.add_argument(--window-size1920,1080) # 登录态持久化 options.add_argument(--user-data-dir/tmp/course_profile) driver webdriver.Chrome(optionsoptions) wait WebDriverWait(driver, 20) driver.get(https://course-platform.example.com/login) # 如果是二维码登录手动扫码后等待跳转 wait.until(EC.url_changes(driver.current_url)) # 进入课程详情页 course_url https://course-platform.example.com/course/1024/practices driver.get(course_url)登录这步有个细节如果你不需要持久化也可以在登录成功后用 driver.get_cookies() 把 cookie 存到本地文件下次启动时用 add_cookie() 灌回去。两种方案都行前者更省事因为 JS 写入的 localStorage 状态也能保留后者更可控适合多账号切换的场景。4.2 遍历题目列表与定位题干进入练习页后先观察页面的结构。我拿到的页面大概长这样左侧是题目进度列表右侧是答题区域题目区域包含题干、选项、已选状态或提交状态。按 F12 逐个检查这些元素定位选择器。这个环节的通用解是question_cards wait.until( EC.presence_of_all_elements_located( (By.CSS_SELECTOR, div.practice-question) ) ) print(f发现 {len(question_cards)} 道题目)注意这里用的是 presence_of_all_elements_located意思是所有匹配元素出现在 DOM 中即可不要求可见。实际做题时可能有分页或懒加载一页只显示几题这样拿到的就不是全部题目。遇到这种情况要么翻页遍历要么检查是否有一个“全部题目列表”的侧栏能快速切换题目索引直接把题目编号对应的题干都读一遍。4.3 提取文本、选项与提交状态遍历题目卡片对每个卡片做一次字段提取。这类页面结构通常稳定直接查子节点for idx, card in enumerate(question_cards, start1): try: title card.find_element(By.CSS_SELECTOR, div.question-title).text options [ opt.text for opt in card.find_elements( By.CSS_SELECTOR, li.option-item ) ] # 状态可能以图标样式或文本形式存在 status_el card.find_elements( By.CSS_SELECTOR, span.status-text ) status status_el[0].text if status_el else 未作答 print(idx, title, |, status) except Exception as e: print(f第 {idx} 题解析失败: {e}) continue提文本这种操作看着简单实际上有几个坑。第一题目里混入公式图片、代码块、表格text 属性只拿到可见文本数学公式取不到。第二选项文本带富文本标签时比如加粗、上标get_attribute(textContent) 比 .text 更稳。第三状态字段不一定是文本可能是一个 icon 的 class 名变化需要去读 class 属性做判断。遇到这些情况我的建议是统一存 HTML 或 textContent别只依赖 .text宁可多存一点粗糙数据也别漏了关键信息。4.4 数据落盘与后续整理Python 里最顺手的就是 pandas 加 openpyxl直接把数据写进 Excelimport pandas as pd rows [] for idx, card in enumerate(question_cards, start1): ... rows.append({ 题号: idx, 题干: title, 选项: |.join(options), 状态: status, }) df pd.DataFrame(rows) df.to_excel(practice_questions.xlsx, indexFalse)如果抓出来的数据量比较大或者需要增量更新用 SQLite 更合适。每轮抓取写一个带时间戳的批次字段后续查询时就能对比差异。这个做法在常规文档里很少提到但在数据增量同步场景中非常实用import sqlite3 conn sqlite3.connect(practice.db) df[batch_time] 2024-06-01 10:00:00 df.to_sql(questions, conn, if_existsappend, indexFalse)数据落盘之后建议顺手做一轮去重和一致性检查。比如同一道题在两个批次中题干完全一致但选项被平台调整了顺序就可以标记出来提醒自己注意而不至于默默丢数据。5. 常见问题与排查小结5.1 元素找不到的三种典型情况这类问题占我调试时间的一半以上主要是三种情况页面还没渲染完元素压根没出现动态 id 每次刷新都会变化元素在 iframe 内部当前上下文根本看不到它。页面没渲染完用显式等待解决。动态 id 需要换个稳定的静态属性来定位比如 class 或>iframe wait.until( EC.presence_of_element_located((By.TAG_NAME, iframe)) ) driver.switch_to.frame(iframe) # 操作完记得切回来 driver.switch_to.default_content()还有个不太容易察觉的场景平台面板中的滚动加载元素虽在 DOM 中但尚未渲染进可视区Selenium 的 is_displayed() 可能返回 false直接执行 click() 会报元素点不到。解决办法是先 scrollIntoViewdriver.execute_script( arguments[0].scrollIntoView({block:center});, element )5.2 页面元素被遮挡导致点击失败弹层、悬浮广告、底部的客服浮窗都会在物理上挡住目标按钮。Selenium 基于元素中心点执行原生点击一旦遮挡就报元素点击被拦截。最稳妥的方式是先等遮挡物消失或者主动关闭浮层。浮层关闭按钮经常是静态 class 隐藏在页面深处直接定位并点击即可。不行再用 JavaScript 点击绕过。但如果网站的弹窗是 fixed 定位盖在上面直接把目标元素滚动到可见区的方案不一定奏效必须把弹层干掉。5.3 弹窗、iframe、新标签页三个坑这三类问题每个都是真实踩坑的高频点。弹窗分 JavaScript alert 和自定义模态框两种。alert 弹窗用 switch_to.alert.accept() 处理自定义模态框就当成一个普通 DOM 去找它的关闭按钮。iframe 我在前面已经讲过遇到 Selenium 找不到内部元素时先检查元素嵌套层数逐层 switch_to.frame 进入。某些网页有多个同层级 iframe需要根据 name 或 src 里的关键字精准切换。新标签页的跳转机制也容易让人懵。某个点击会触发 target_blank 打开新页面但 driver 当前还是停留在旧页面。需要先获取所有窗口句柄切到新窗口main_window driver.current_window_handle # 点击操作触发新标签页 new_window [w for w in driver.window_handles if w ! main_window][0] driver.switch_to.window(new_window)用完记得切回主窗口免得后续操作鬼打墙。5.4 常见问题速查表我把实战中碰到的高频问题整理成一张表方便你粘贴到自己的笔记里速查问题典型原因解决方案元素找不到 NoSuchElementException页面未加载完成 / 选择器错误显式等待 在 Console 验证选择器元素不可见 ElementNotInteractableException元素被遮挡 / 在视口外scrollIntoView 或先关闭浮层点击不生效但无报错事件绑定未触发 / 元素覆盖用 execute_script click 或派发鼠标事件序列内容在 iframe 中未切换 frame 上下文switch_to.frame 进入操作页面跳转后定位失效旧元素引用失效重新 find 元素别保存陈旧引用浏览器版本升级驱动失效驱动版本不匹配webdriver-manager 自动安装登录状态频繁失效平台风控 / cookie 过期持久化 user-data-dir 降低触发频率偶发的超时网络波动 / 资源加载慢循环重试最多重试 3 次间隔指数递增5.5 几个值得留意的细节页面长时间连续操作后被平台判定为异常这种问题和代码关系不大更多是操作频率问题。我自己的节奏是每步操作之间控制一点间隔别做成“无停顿连环点”的机器样。还有个小细节每跑完一轮记得 driver.quit()把进程和连接彻底关掉。只调 close() 的话旧的浏览器进程有可能残留时间长了系统一堆僵尸进程占内存而且下一次启动容易出怪问题。6. 边界与扩展同样的技术换个方向就是好工具6.1 合规边界必须自己守住一开始我就说了标题里那个“抓答案”的方向不适合直接执行。作为在这个领域做过不少项目的从业者我得把话讲透自动化脚本本身没有原罪但用它去获取非授权数据、绕过付费墙、破解验证码、侵犯他人账号信息都属于越界。在线学习平台上的作业和测验是课程设计者教学考核的一部分答案和评分逻辑属于平台和教师的合法权益脚本批量收集并公开传播轻则账号封禁重则涉及法律风险。所以我现在做练习项目固定两个原则第一只操作自己账号权限范围内的内容第二只做“把数据拿回来整理成对自己学习有用的形态”这类工作不碰修改成绩、批量提交答案这类危害公平性的操作。这不是唱高调而是吃过亏的人回头看才想明白的账。批量抓取这类行为一旦对平台运营造成明显压力平台升级风控、改动页面结构最后买单的还是你自己——不仅要重新适配代码账号还可能被封得不偿失。6.2 把技术迁移到正经场景这整套技术栈——页面自动化、数据提取、信息落盘——换一层皮就是通用的自动化工作流。我把它迁移到过好几个方向效果都很好自动化巡检每天定时打开业务后台检查各核心页面关键数据是否正常发现数值异常自动截图并推送提醒。这个方向用到的 Selenium 技术跟抓练习题的流程完全一样只是把“抓题干”换成“抓指标”。数据报表汇总把分散在多个系统的数据页面按流程打开、取数、合并形成一张总表替代人工每天复制粘贴的操作。线上课程质量保障自动化模拟学生视角把课程视频播一遍检查是否存在资源失效、样式错乱、练习题无法提交等问题。这正是基于 Selenium 的常用自测思路。如果你所在团队在做教学平台或在线培训系统可以尝试把上方这套流程改造成平台的自检工具让测试人员不用重复走一遍完全相同的课程学习路径。这些方向的共性特征是重复、固定、人工操作成本高、出错概率大。只要满足这四个特征就是 Selenium 适合介入的场景。判断要不要用它标准很简单——如果一个熟练操作者每天都要花半小时做同一件事那这事就该交给脚本去做。7. 最后想说的几句实在话写完这套自动化流程我最深的体会是Selenium 本身不难真正难的是“等你真正搞清楚页面为什么这么设计的时候你会比写十页脚本收获更多”这件事。每一次定位失败、每一次等待超时背后都是前端渲染逻辑、事件机制、网络时序三者的配合。你把问题一个个查过去实际上是在无意中补一门“网页运行原理”的课。给想上手的朋友一个建议别一上来就追求什么框架、并发、高可用先用最简单的方式把一条主流程跑通。跑通之后再考虑增加异常处理、重试机制、日志记录这些工程化能力。等你能稳定做出一个能连续跑三天不出错的自动化任务时你再回看这个项目标题是什么已经不重要了因为你手里真正掌握的是“让浏览器替你干活”这套通用能力。顺手再分享一个耐用的习惯长期跑的脚本建议把控制台输出直接重定向到文件加上时间戳方便事后回溯。配合简单的断言逻辑——每个关键节点确认操作结果符合预期——就能让脚本从“能跑”进化到“能稳定跑”。这才是这类技术项目真正值得花时间的部分。