简介:基于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 而不是 requests
2.1 requests 失效的根源:动态渲染与接口加密
先说一个很多初学者翻车的点。考试宝这类在线题库页面,打开网址后浏览器里能看到题目,但requests.get()拿到的 HTML 源码里根本搜不到题干文字。原因在于数据是由 JavaScript 异步请求接口后动态插入 DOM 的,初始 HTML 只是个空壳子。有的接口能直接分析出来,返回 JSON 数据;但考试宝的接口带签名参数,直接构造请求需要逆向加密逻辑,成本远高于表面看起来的“一个 GET 请求”。
这种情况下 selenium 的优势就很明显了。它直接驱动真实浏览器加载页面,等 DOM 渲染完成后通过find_element系列方法提取内容,绕开了接口加密这道坎。代价是速度慢、资源占用高,但换取的是稳定性和易用性。这份源码采用的策略就是用 selenium 模拟用户浏览,再配合显式等待确保元素加载完成。
2.2 环境搭建:驱动版本是第一个坑
把源码跑起来前,先把环境列清楚。项目基于 Python 3,selenium 库从 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(service=service)参数说明:executable_path是旧版指定驱动位置的写法,改成Service后逻辑不变,但能消除弃用告警。驱动可以放在项目目录下,也可以放系统 PATH 里,放项目目录更便于整个源码包迁移。
2.3 核心依赖明细与版本建议
除了 selenium,源码还会用到标准库里的time、json、re,以及用来整理数据的csv。第三方依赖只有 selenium,这对毕业设计来说是个加分项——不用在环境依赖上花太多时间。
| 组件 | 版本建议 | 用途 |
|---|---|---|
| Python | 3.7 及以上 | 运行环境 |
| selenium | 4.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会一直轮询页面,直到找到至少一个class="question-item"的元素才算成功。如果 10 秒内没出现,抛超时异常。相比time.sleep(3)这种固定等待,显式等待在响应快的页面上不浪费时间,在慢页面上也不会抢跑。
参数说明:10是超时秒数,网络状况差可以调到 15;By.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[@class='question-title']").text options = item.find_elements(By.XPATH, ".//div[@class='option-item']") option_list = [opt.text for opt in options] return title, option_list逻辑说明:item是上面拿到的试题元素,.//div[@class='question-title']里的点表示从当前元素往下找,不会误匹配到外层其他同名元素。先用find_element拿题干,再用find_elements拿全部选项。find_element返回单个 WebElement,find_elements返回列表,两者的区别在写爬虫时经常搞混。
参数说明:'.//div[@class="question-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[@class='next-page']")) ) next_btn.click() wait.until(EC.staleness_of(next_btn)) except TimeoutException: break逻辑说明:staleness_of等待旧按钮失效,这相当于一个隐式的新页面加载完成信号。如果“下一页”按钮不存在或不可点击,说明已经到最后一页,循环终止。这个写法比判断页码数字更省心,因为不用关心总页数。
参数说明:"//a[@class='next-page']"是“下一页”按钮的定位,不同网站这个选择器完全不同。实战时建议先在浏览器开发者工具里用 Elements 面板确认按钮的真实标签和 class 名。
3.4 数据落地:JSON 和 CSV 双格式输出
抓下来的试题不能只存在内存里。源码里通常把数据拼成字典列表,然后一次性写入文件。这里我倾向于 JSON 和 CSV 各写一份,JSON 保留完整结构,CSV 方便 Excel 打开看。
def save_to_json(data, filename="exams.json"): with open(filename, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) def save_to_csv(data, filename="exams.csv"): with open(filename, "w", encoding="utf-8-sig", newline="") as f: writer = csv.DictWriter(f, fieldnames=["title", "options"]) writer.writeheader() writer.writerows(data)逻辑说明:JSON 写入时ensure_ascii=False是关键,不写这个中文会变成\uXXXX转义字符,看着头疼。CSV 写入用utf-8-sig编码是给 Excel 用的——不加 BOM 的 UTF-8 CSV 用 Excel 打开全是乱码,这个坑特别常见。newline=""防止 Windows 下出现空行。
参数说明:indent=2控制 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[@class='next-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_file="cookies.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", encoding="utf-8")) print(f"抓取题目数: {len(data)}")如果总数对不上,优先排查两种可能:一是翻页循环提前 break,next_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 友好格式。
解决:打开文件时统一加encoding="utf-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的稳定等待策略,考试宝这种动态页面的批量抓取可以做成完全自动化的流程。从那以后我每次写爬虫,都会强制把断点记录、去重和异常恢复这三件事写进主循环里,哪怕目标只是几十条数据——因为有了一次跑了一半崩掉的经历,你就知道所谓“自动化”最怕的就是不可重入。这份源码给你的是骨架,往里填什么逻辑,取决于你想让这个采集器变得更抗造还是更快。希望帮到你。
本文还有配套的精品资源,点击获取