Selenium爬虫实战:破解考试宝动态渲染的批量试题抓取方案
发布时间:2026/10/7 13:53:02 锦皓数字建站

简介基于Python与selenium框架的考试宝试题爬虫项目面向计算机相关专业需要完成大作业、毕业设计或爬虫实战练习的学生。项目源码经导师指导并评审通过曾获98分高分核心实现包含试题页自动抓取、模拟登录与数据解析等典型流程适合作为课程设计参考或爬虫入门进阶案例。包体共6个文件压缩包约8.2MB包含2个Python源码文件主程序与配置文件、1个chromedriver.exe驱动、1个README说明文档、1张效果示意图及1个Git忽略文件结构与用途清晰可快速解压运行。目前已有273人学习下载。借助随包的使用说明与代码注释使用者可以掌握selenium浏览器自动化、页面元素定位与试题数据提取的完整思路并能根据自身需求修改目标页面与存储逻辑直接复用到相关爬虫项目中。1. 从“复制粘贴”到“可复用爬虫考试宝试题抓取为什么值得拆”做毕业设计或者技术练手的时候很多人会遇到一个尴尬场景目标网站看起来数据都在页面上但用requests写好的脚本一跑返回的 HTML 里啥也没有——内容全在 JavaScript 里动态渲染。考试宝这类在线题库站点正是典型试题、选项、答案解析全部靠前端异步加载。这时候 Python 加 selenium 的组合就派上用场了。我拆的这份基于 Python 实现的考试宝试题爬虫源代码核心就是 selenium 框架配合一套完整的页面等待、元素定位和结构化导出流程能从题库页面把试题批量抓下来。对正在做 Python 毕业设计、需要实战爬虫源码参考或者想搞明白 selenium 到底怎么落地而不是停留在webdriver.Chrome()这一步的人来说这是份能直接跑、跟着改就能用的料。2. 爬虫选型与运行环境为什么是 selenium 而不是 requests2.1 requests 失效的根源动态渲染与接口加密先说一个很多初学者翻车的点。考试宝这类在线题库页面打开网址后浏览器里能看到题目但requests.get()拿到的 HTML 源码里根本搜不到题干文字。原因在于数据是由 JavaScript 异步请求接口后动态插入 DOM 的初始 HTML 只是个空壳子。有的接口能直接分析出来返回 JSON 数据但考试宝的接口带签名参数直接构造请求需要逆向加密逻辑成本远高于表面看起来的“一个 GET 请求”。这种情况下 selenium 的优势就很明显了。它直接驱动真实浏览器加载页面等 DOM 渲染完成后通过find_element系列方法提取内容绕开了接口加密这道坎。代价是速度慢、资源占用高但换取的是稳定性和易用性。这份源码采用的策略就是用 selenium 模拟用户浏览再配合显式等待确保元素加载完成。2.2 环境搭建驱动版本是第一个坑把源码跑起来前先把环境列清楚。项目基于 Python 3selenium 库从 pip 安装就行但浏览器驱动是另一个独立的东西。pip install selenium装完 selenium 之后还需要对应浏览器的 WebDriver。Chrome 的话就是chromedriver注意版本必须和本机 Chrome 主版本号匹配。比如本机 Chrome 是 114 版就下 114 版的 chromedriver版本不匹配时程序会在webdriver.Chrome()这行直接报 session 创建失败。from selenium import webdriver driver webdriver.Chrome(executable_path./chromedriver)这段代码里executable_path指定本地驱动路径。新版 selenium 4.x 里这个参数已经标记为弃用更推荐用Service对象源码在兼容性处理上一般会保留旧写法实际使用可以改成from selenium import webdriver from selenium.webdriver.chrome.service import Service service Service(executable_path./chromedriver) driver webdriver.Chrome(serviceservice)参数说明executable_path是旧版指定驱动位置的写法改成Service后逻辑不变但能消除弃用告警。驱动可以放在项目目录下也可以放系统 PATH 里放项目目录更便于整个源码包迁移。2.3 核心依赖明细与版本建议除了 selenium源码还会用到标准库里的time、json、re以及用来整理数据的csv。第三方依赖只有 selenium这对毕业设计来说是个加分项——不用在环境依赖上花太多时间。组件版本建议用途Python3.7 及以上运行环境selenium4.x 或 3.141 均可浏览器自动化Chrome/Edge最新稳定版被驱动的浏览器chromedriver/edgedriver与浏览器大版本一致浏览器驱动我自己习惯用 Chrome 配 chromedriver遇到版本问题就去查看浏览器chrome://version里的版本号再去对应站点下载匹配的驱动。3. 源码核心拆解从页面加载到试题结构化导出3.1 初始化与页面等待别让“元素找不到”毁了你的脚本爬虫代码中大量异常都出在元素还没加载完成就去查找。考试宝页面上的试题列表是异步渲染的如果不加等待NoSuchElementException会频繁出现。源码里用的是显式等待而不是写死的time.sleep()。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By wait WebDriverWait(driver, 10) question_items wait.until( EC.presence_of_all_elements_located( (By.CLASS_NAME, question-item) ) )逻辑说明WebDriverWait(driver, 10)创建一个最长等 10 秒的等待器presence_of_all_elements_located会一直轮询页面直到找到至少一个classquestion-item的元素才算成功。如果 10 秒内没出现抛超时异常。相比time.sleep(3)这种固定等待显式等待在响应快的页面上不浪费时间在慢页面上也不会抢跑。参数说明10是超时秒数网络状况差可以调到 15By.CLASS_NAME是定位方式换成By.XPATH或By.CSS_SELECTOR也可以question-item是实际元素类名随着目标网站改版需要更新。3.2 解析题面与选项XPATH 相对路径的写法定生死页面里每条试题的结构通常是题干一个 div、选项列表一个 div、答案区域一个 div。用绝对路径写 XPATH 很容易崩因为页面改一行布局整个脚本就废了。源码里的解析逻辑更倾向于用相对路径 元素层级关系定位。def parse_question(item): title item.find_element(By.XPATH, .//div[classquestion-title]).text options item.find_elements(By.XPATH, .//div[classoption-item]) option_list [opt.text for opt in options] return title, option_list逻辑说明item是上面拿到的试题元素.//div[classquestion-title]里的点表示从当前元素往下找不会误匹配到外层其他同名元素。先用find_element拿题干再用find_elements拿全部选项。find_element返回单个 WebElementfind_elements返回列表两者的区别在写爬虫时经常搞混。参数说明.//div[classquestion-title]是最常用的相对定位写法表示按属性匹配。如果页面结构里标题的 class 是动态生成的比如question-title-12345那就要改成模糊匹配.//div[contains(class, question-title)]。3.3 翻页控制循环与终止条件缺一不可考试宝的列表页用分页器控制爬虫需要遍历所有页。常见的翻页有两种点击页码按钮和修改 URL 参数。源码处理方式是定位“下一页”按钮并点击然后重新等待元素加载。while True: parse_current_page(driver) try: next_btn wait.until( EC.element_to_be_clickable((By.XPATH, //a[classnext-page])) ) next_btn.click() wait.until(EC.staleness_of(next_btn)) except TimeoutException: break逻辑说明staleness_of等待旧按钮失效这相当于一个隐式的新页面加载完成信号。如果“下一页”按钮不存在或不可点击说明已经到最后一页循环终止。这个写法比判断页码数字更省心因为不用关心总页数。参数说明//a[classnext-page]是“下一页”按钮的定位不同网站这个选择器完全不同。实战时建议先在浏览器开发者工具里用 Elements 面板确认按钮的真实标签和 class 名。3.4 数据落地JSON 和 CSV 双格式输出抓下来的试题不能只存在内存里。源码里通常把数据拼成字典列表然后一次性写入文件。这里我倾向于 JSON 和 CSV 各写一份JSON 保留完整结构CSV 方便 Excel 打开看。def save_to_json(data, filenameexams.json): with open(filename, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) def save_to_csv(data, filenameexams.csv): with open(filename, w, encodingutf-8-sig, newline) as f: writer csv.DictWriter(f, fieldnames[title, options]) writer.writeheader() writer.writerows(data)逻辑说明JSON 写入时ensure_asciiFalse是关键不写这个中文会变成\uXXXX转义字符看着头疼。CSV 写入用utf-8-sig编码是给 Excel 用的——不加 BOM 的 UTF-8 CSV 用 Excel 打开全是乱码这个坑特别常见。newline防止 Windows 下出现空行。参数说明indent2控制 JSON 缩进不打成一行方便人读fieldnames必须和字典里的键名一致否则writerows会静默丢字段。4. 实操复现步骤从上手到跑通全流程4.1 拿到源码后的目录结构与启动顺序源码包的目录一般包含两部分.py文件是主体逻辑.md或.txt是使用说明。按我的经验第一次跑通要用以下顺序操作。# 1. 安装依赖 pip install selenium # 2. 配置驱动路径macOS/Linux 示例 chmod x chromedriver # 3. 运行主脚本测试单页抓取 python exam_crawler.py --test第一步是装依赖源码基于 selenium其他库大概率是 Python 标准库。第二步给驱动加执行权限Windows 上不用这一步但要把chromedriver.exe放到 PATH 或脚本同目录。第三步先用测试模式跑单页确认能抓到题目再全量跑这个习惯能帮你少踩很多坑。4.2 核心配置文件调整URL 与目标元素源码里通常都有一个配置区域或者是单独的config.py。这里集中维护目标 URL 和选择器改动时不用翻业务代码。# config.py TARGET_URL https://example.com/exam/list QUESTION_CLASS question-item TITLE_CLASS question-title OPTION_CLASS option-item NEXT_BUTTON_XPATH //a[classnext-page] PAGE_WAIT_SECONDS 10逻辑说明把选择器抽成常量后续目标网站改版时只需要改这一个文件不必动parse_question那些函数。这也是源码结构比入门脚本“高级”的地方——带工程化的影子。参数说明TARGET_URL是列表页入口考试宝实际 URL 需要在浏览器里打开目标题库后从地址栏复制PAGE_WAIT_SECONDS对应WebDriverWait的超时时间网络慢的环节调大。4.3 模拟登录场景Cookie 复用方案考试宝的部分题库需要登录才能查看完整题目源码里如果涉及登录大概率提供两种思路自动登录和 Cookie 复用。自动登录要处理验证码复杂度高得多Cookie 复用是自己先在浏览器里手动登录一次再把 Cookie 传给 selenium。import pickle def load_cookies(driver, cookie_filecookies.pkl): with open(cookie_file, rb) as f: cookies pickle.load(f) for cookie in cookies: driver.add_cookie(cookie)逻辑说明pickle把 Cookie 列表序列化到本地文件下次启动时加载回浏览器。注意必须在访问目标域名之后才能add_cookie先driver.get(TARGET_URL)跳转一遍再注入 Cookie最后刷新页面才会带着登录态。参数说明cookie_file是本地文件名Cookie 有有效期过期后重新手动登录并重新保存即可。这个方案比在代码里硬编码账号密码体面得多。4.4 数据完整度检查抓完后怎么确认没漏题跑完脚本后不要急着收工。我用一个简单粗暴的检查方式对比 JSON 里的题目数量和页面底部的总数统计。import json data json.load(open(exams.json, encodingutf-8)) print(f抓取题目数: {len(data)})如果总数对不上优先排查两种可能一是翻页循环提前 breaknext_btn定位失败导致中途退出二是页面里部分题目是懒加载需要滚动页面才触发渲染。懒加载这个情况我吃过亏列表页下滑时才会加载新题目这时候要先driver.execute_script(window.scrollTo(0, document.body.scrollHeight))滚到底部再开始解析。5. 避坑指南selenium 爬虫高频踩坑记录5.1 坑一element not interactable异常现象调用next_btn.click()时报错提示元素当前状态不可交互。原因元素在 DOM 里存在但被其他元素遮挡或者还在动画过渡中点击事件无法命中。解决先用driver.execute_script(arguments[0].scrollIntoView(true);, next_btn)把元素滚到可视区域再执行点击。如果还不行改用 JavaScript 直接触发点击driver.execute_script(arguments[0].click();, next_btn)这段代码绕过了元素遮挡判断直接派发点击事件。副作用是跳过了正常用户操作的事件链路但绝大部分场景下没问题。5.2 坑二抓取中途浏览器窗口被关闭现象脚本跑了几十页突然报WebDriver会话失效浏览器窗口没了。原因页面异常跳转导致浏览器崩溃或者内存占用过高被系统杀掉。尤其是用了 headless 模式加长跑任务稳定性远不如有头模式。解决给主循环加一层文件记录每抓完一页就记录当前页码到progress.txt下次启动时从记录页继续。另外一个实用操作是给 Chrome 加禁用 GPU 的启动参数能显著降低无头模式崩溃率。from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--disable-gpu) options.add_argument(--no-sandbox)参数说明--disable-gpu关闭硬件加速对纯文本页面没有任何影响--no-sandbox在 Linux 服务器上几乎必须加本机 Windows 不加也能跑。5.3 坑三中文乱码与编码错误现象写入 CSV 后用 Excel 打开是乱码或者 JSON 文件里全是\u开头的转义。原因open()时没指定编码或者 CSV 用了utf-8而不是 Excel 友好格式。解决打开文件时统一加encodingutf-8写 CSV 用utf-8-sig。从元素读取文本时text属性本身不会乱码乱码只出现在文件写入环节。5.4 坑四元素定位总是命中同第一个元素现象find_element返回的内容永远是第一道题循环遍历无效。原因定位选择器作用域出了问题。用了全局路径//div而不是相对路径.//div导致每次从页面根开始找找到的永远是第一个。解决改成item.find_element(By.XPATH, .//div[class...])而不是driver.find_element(By.XPATH, //div[class...])注意前者的点是灵魂。5.5 坑五脚本跑太久被目标站点封 IP现象抓了 200 多题后页面开始弹出验证码或者直接拒绝访问。原因请求频率过高触发了服务端的访问控制策略。解决降低翻页速度。每页之间随机等 2 到 4 秒避免固定间隔。另外关闭浏览器窗口后重启 driver定期清理会话状态也会比单会话跑到底更稳。6. 进阶玩法把单页脚本改成可断点续跑的采集器源码包的核心功能是考试宝试题抓取但把它改造成一个通用采集器其实不难。我一般会在原脚本基础上做三件事第一用progress.txt记录断点第二把解析函数独立出来方便换成别的网站结构第三加数据去重。断点续跑的逻辑是每处理完一页把当前页数写入文件。下次启动时先读这个文件从记录页继续而不是从头再来。对考试宝这种分页很多的题库这个功能能省掉大量重跑时间。去重逻辑更简单维护一个seen集合每道题的题干文本算一个哈希值存进集合里。如果新题目哈希已经存在跳过不写文件。import hashlib def question_hash(title): return hashlib.md5(title.encode(utf-8)).hexdigest() seen set() for item in question_items: title parse_title(item) h question_hash(title) if h in seen: continue seen.add(h) result.append({title: title, options: parse_options(item)})这套代码的改进价值在于不再把爬虫当成一次性运行脚本而是当成批处理任务来管理。配合selenium的稳定等待策略考试宝这种动态页面的批量抓取可以做成完全自动化的流程。从那以后我每次写爬虫都会强制把断点记录、去重和异常恢复这三件事写进主循环里哪怕目标只是几十条数据——因为有了一次跑了一半崩掉的经历你就知道所谓“自动化”最怕的就是不可重入。这份源码给你的是骨架往里填什么逻辑取决于你想让这个采集器变得更抗造还是更快。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。