
简介本资源是一套基于Python与Selenium框架实现的考试宝试题爬虫实战项目面向计算机相关专业本科生及自学开发者专为毕业设计、课程大作业与爬虫技术进阶练习打造。项目已通过导师评审并获98分高分代码经本地完整编译与多轮调试确保开箱即用涵盖登录模拟、题库翻页、题目与选项结构化解析等核心功能模块。压缩包共6个文件含2个核心Python脚本main.py与settings.py、1个预配置ChromeDriver可执行文件、1份详细README.md使用说明、1张运行效果截图及1个.gitignore配置文件整体大小为8.2MB。目前已有265人学习下载配套文档清晰标注环境依赖、运行步骤与常见问题处理逻辑特别适合缺乏真实项目经验的学习者快速掌握动态网页爬取、浏览器自动化与结构化数据提取等关键技术点。 前阵子备考需要刷大量练习题我就想从考试宝这类在线题库里把公开的免费题目批量抓下来。一开始我以为用requests就能搞定结果打开目标页面一看源码里的题目数据是用JavaScript动态渲染的直接请求只能拿到一个空壳框架。后来换成selenium模拟真实浏览器问题一下就通了。这篇就把整套基于Python selenium的考试宝试题爬虫源码结构和使用说明整理出来顺便聊聊我为什么选selenium、从“能跑”改成“能长期稳定跑”时踩过哪些坑。这个项目适合两类人一类是想快速拿到公开题库数据做学习研究的朋友另一类是想入门selenium爬虫、但又不想一头扎进接口签名逆向的Python初学者。它会教你怎么用真实浏览器把动态页面里的内容稳定抓下来并且整理成可复现的代码结构和操作流程。1. 为什么选择selenium而不是requests1.1 考试宝这类页面的数据不在HTML源码里很多人刚开始写爬虫第一反应就是requests拿HTML然后用正则或者lxml提取数据。这套思路对付静态网站没问题但考试宝这种题库平台的页面打开浏览器开发者工具F12就能看到题目列表区域的HTML一开始是空的数据是通过Ajax异步请求灌进去的。直接用requests抓下来的页面里面只有一段段压缩过的JS代码。我最初也尝试过分析它的异步接口想着找到那个返回题目JSON的XHR请求直接构造参数模拟调用。结果发现接口地址带签名、请求参数做了加密而且不同页面参数还不一样。如果硬啃两天都未必能跑通。换成selenium之后根本不需要关心后端接口怎么签名因为浏览器已经把页面渲染好了题目文本、选项、图片就摆在DOM里我只需要定位元素、提取文本逻辑一下子从“逆向工程师”降级成了“页面操作员”。1.2 selenium的取舍慢但稳selenium最大的缺点是慢因为每执行一步都要启动一次完整的浏览器、加载页面资源、渲染样式。批量抓一千道题可能要跑几十分钟。但这在个人备考场景下完全能接受毕竟不是每天都要百万级抓取。另一个优势是开发效率高。requests需要你理解整个网络链路的细节包括Cookie怎么保持、请求头怎么伪造、签名怎么生成selenium则是“所见即所得”我先用浏览器手动点一遍把关键元素记录下来然后让代码照着做。下面是我在实际选型时的对比表维度requestsselenium页面加载JS不执行拿到的是原始HTML完整执行拿到的是渲染后的DOM接口签名逆向通常需要做工作量大不需要浏览器已经处理开发效率中等调试成本高高逻辑直观资源占用低高每个实例都是完整浏览器适用场景静态页面、可预测的API动态渲染、JS交互多的页面对考试宝这个项目来说慢一点无所谓稳定拿到题目才重要。我的选择很直接能用selenium解决就别去死磕接口签名。1.3 明确数据边界只抓公开内容动手之前我先确认了一件事只抓取未登录状态下就能看到的公开题目不碰需要登录权限、会员权限的数据。这既是技术上省事也是合规上稳妥。我不考虑通过任何方式去绕过权限限制也不需要去破解验证码。如果遇到需要登录或验证码的页面我会直接停手因为那不是我这个项目需要的数据范围。这也给代码设计带来一个好处不用处理复杂的登录状态和验证码逻辑整个流程就简化成“打开列表页 - 等待内容加载 - 提取题目 - 翻页”四条步骤。2. 环境准备与selenium核心机制2.1 环境搭建Python、虚拟环境和依赖第一步是准备Python环境。我建议使用Python 3.9以上的版本并且创建一个独立的虚拟环境避免和系统全局包互相污染。python -m venv venv source venv/bin/activate # Windows下是 venv\Scripts\activate然后安装核心依赖pip install selenium webdriver-manager pandas这里说下webdriver-manager。以前selenium最折腾的地方是手动下载chromedriver还要保证驱动和浏览器版本一致版本一升级就报SessionNotCreatedException。webdriver-manager会自动检测你本地Chrome版本并下载匹配的驱动省掉了大量重复劳动。这个库我强烈推荐装。2.2 WebDriver初始化与常用启动参数selenium本身只是一个操作浏览器的协议库真正干活的是浏览器驱动。用Chrome浏览器就需要chromedriver。通过webdriver-manager初始化驱动的代码可以这样写from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options from webdriver_manager.chrome import ChromeDriverManager def create_driver(headlessFalse): options Options() if headless: options.add_argument(--headlessnew) options.add_argument(--window-size1920,1080) options.add_argument(--disable-gpu) options.add_argument(--no-sandbox) options.add_argument(--langzh-CN) service Service(ChromeDriverManager().install()) return webdriver.Chrome(serviceservice, optionsoptions)这里注意两点。第一headless模式无头模式适合在服务器上跑但没有界面遇到某些元素被遮挡或没有正常渲染的情况排查起来会比较费劲所以我调试阶段会先关掉headless。第二--window-size参数很重要很多前端页面在窄窗口下布局会变化导致元素被折叠或不可见固定窗口大小可以让行为保持稳定。2.3 显式等待是爬虫稳定的灵魂刚上手selenium时我习惯用time.sleep(2)让页面加载。但这个做法非常不优雅网络慢的时候2秒不够快的时候又白白浪费时间。更稳的写法是显式等待明确告诉浏览器“等到某个元素出现再继续”。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) card wait.until( EC.presence_of_element_located((By.XPATH, //div[classquestion-card])) )presence_of_element_located表示元素已经出现在DOM中但不一定可见如果点击前还需要元素可点就用element_to_be_clickable。别小看这几个等待条件我后来遇到的大部分偶发报错都是因为用了固定sleep而不是等待条件。2.4 元素定位XPath为什么是首选selenium定位元素有ID、class、CSS选择器、XPath很多种方式。ID最快但很多页面元素没有稳定的ID。class名如果是CSS模块化生成的长串比如_abc123_question_xyz直接复制非常容易失效。XPath的优势在于可以写相对路径比如//div[contains(class, question)]//p[classtext]只要部分class名稳定就不会断。我的习惯是能用相对XPath就不用绝对路径绝对路径类似/html/body/div[3]/div[2]/div[1]只要前辈一个元素的顺序变化就得重写。定位时我会先截取页面里题目区域的最外层容器再往里逐层找这样即使外层结构调整只要容器class不变内部依然能定位到。3. 核心代码结构与使用说明从零搭一个能跑的爬虫3.1 项目目录设计这个项目的代码结构我特意拆成了几个模块而不是全部塞在一个脚本里。原因很简单爬虫项目一旦写长环境配置、页面交互、数据解析、数据保存如果都堆在一起维护起来非常痛苦。exam_crawler/ ├── config.py # 全局配置目标URL、延时范围、最大翻页数 ├── driver.py # 浏览器驱动初始化 ├── crawler.py # 主控制流程翻页、调度各个模块 ├── parser.py # 页面解析从DOM中提取题目 ├── pipeline.py # 数据持久化写CSV/JSON ├── requirements.txt # 依赖清单 ├── README.md # 使用说明 └── output/ └── questions.json # 抓取结果输出目录这样的分层和公司里的自动化测试框架类似driver负责“浏览器怎么启动”parser负责“看到内容怎么拿”pipeline负责“拿到内容怎么存”crawler负责“整个流程怎么走”。每一层都能单独测试出问题也不会互相牵连。3.2 核心代码页面解析和保存以考试宝的公开题库列表页为例页面结构大致是列表页中有多个题目卡片每个卡片包含题目文本、选项A/B/C/D、题目类型等。解析模块可以这样写# parser.py from selenium.webdriver.remote.webelement import WebElement def parse_question_card(card: WebElement) - dict: question card.find_element(By.CSS_SELECTOR, .question-text).text type_text card.find_element(By.CSS_SELECTOR, .question-type).text options [ opt.text for opt in card.find_elements(By.CSS_SELECTOR, .option-item) ] return { question: question, type: type_text, options: options, }这里用CSS选择器举例是因为这类文本类元素相对稳定。如果你发现页面结构变化就把选择器换成XPath。另外如果某个选项可能为空建议加一个判断避免空列表混入数据。数据保存模块用pandas最方便# pipeline.py import pandas as pd def save_to_json(data: list, path: str) - None: df pd.DataFrame(data) df.to_json(path, orientrecords, linesTrue, force_asciiFalse)force_asciiFalse一定要加否则中文会变成一串\uXXXX事后排查数据非常麻烦。如果保存为CSV同理记得指定encodingutf-8-sig否则Excel打开会乱码。3.3 主流程翻页、解析、保存主流程放在crawler.py里核心逻辑是一个循环import time import random from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from driver import create_driver from parser import parse_question_card from pipeline import save_to_json MAX_PAGES 20 BASE_URL https://example.com/public-questions?page{} def crawl(): driver create_driver(headlessFalse) wait WebDriverWait(driver, 15) results [] for page in range(1, MAX_PAGES 1): driver.get(BASE_URL.format(page)) # 等待题目卡片出现 wait.until(EC.presence_of_all_elements_located( (By.CSS_SELECTOR, .question-card) )) cards driver.find_elements(By.CSS_SELECTOR, .question-card) for card in cards: try: item parse_question_card(card) item[page] page results.append(item) except Exception as e: print(f第{page}页解析失败: {e}) continue # 随机延时避免请求过快 time.sleep(random.uniform(1, 2)) print(f第{page}页完成累计{len(results)}条) save_to_json(results, output/questions.json) driver.quit() if __name__ __main__: crawl()注意我的翻页逻辑是重新请求page{}参数而不是点击页面里的“下一页”按钮。这样实现简单也不会因为按钮被遮罩挡住而报错。如果目标站点不支持这个URL参数就用点击下一页的方式但每次循环都要重新获取按钮元素因为点击后页面刷新旧引用会失效。3.4 使用说明从克隆到跑通README里的使用说明不需要写几万字但一定要把“跑起来需要几步”说清楚。这个项目的流程是安装Python 3.9创建虚拟环境。执行pip install -r requirements.txt。修改config.py里的BASE_URL和MAX_PAGES。执行python crawler.py。打开output/questions.json检查结果。一定要提醒自己第一次运行别开headless用有头模式看着浏览器自动点哪里卡住立刻就知道。跑通之后再改成headless挂在服务器上。3.5 增加断点续爬和失败重试实际抓取几百页时偶尔会因为网络超时或者页面结构异常中断。我建议在循环里加一个“失败重试”的轻量逻辑def get_with_retry(driver, url, wait, max_retries3): for attempt in range(max_retries): try: driver.get(url) wait.until(EC.presence_of_element_located( (By.CSS_SELECTOR, .question-card) )) return except Exception: if attempt max_retries - 1: raise time.sleep(3)加这个模块之后脚本的容错率明显提升。另外每完成一页可以把当前页码记录到本地文件里下次启动时从断点接着跑这就是最基础的“增量爬虫”思路。4. 高频报错与排查技巧实录4.1 问题速查表报错信息可能原因处理方式SessionNotCreatedException浏览器驱动版本和浏览器不匹配使用webdriver-manager自动匹配驱动版本TimeoutException等待时间太短或元素定位不到检查XPath/CSS选择器增加等待时间上限NoSuchElementException页面结构变化或目标元素未渲染用开发者工具重新确认元素路径不要手写绝对路径ElementClickInterceptedException元素被弹窗、广告或遮罩遮挡先关闭弹窗或用execute_script滚动到元素可见再点击StaleElementReferenceException页面刷新后继续使用了旧元素引用每轮循环重新获取元素对象不要缓存ElementNotInteractableException元素存在但不可点击或不可输入通常是隐藏元素先判断它是否可见再决定是否需要操作4.2 踩坑一headless模式点击失效我一开始为了省资源全程用--headlessnew跑结果发现页面里有些按钮看起来在但调用click()会报ElementClickInterceptedException。排查了半天发现是因为无头模式虽然渲染了DOM但没有真实窗口的视口布局导致按钮实际坐标为(0,0)。解决办法有两个方向一是直接关闭headless模式在本地服务器上跑一个虚拟显示二是使用driver.execute_script(arguments[0].scrollIntoView();, element)先把目标元素滚动到视口再执行点击。我后来在代码里两种方式都做了兜底稳定了很多。4.3 踩坑二翻页之后元素引用失效这个问题也很典型在循环外先获取了next_button driver.find_element(...)点击第一页之后页面DOM刷新再用这个next_button去点击第二页的下一页就报了StaleElementReferenceException。原因是selenium里的WebElement对象不是一个“值”而是一个指向DOM节点的引用。页面一旦刷新旧节点的引用就失效了。方案很简单每次循环内重新查找按钮元素不要保存到循环外。同样的逻辑也适用于题目卡片列表每次都重新获取最新卡片集合避免用一个过期列表遍历。4.4 排查技巧让脚本输出过程信息很多爬虫脚本黑盒运行报错之后只能瞎猜。我习惯在关键节点加日志包括当前访问的URL当前等待的元素是什么当前页面标题或者URL是否发生了变化捕获异常时打印堆栈和页面源码片段。这些输出看起来不起眼但在几百页的抓取任务里能快速定位是网络问题还是元素定位问题。我还会在异常时截图保存到本地driver.save_screenshot(fdebug/{page}_error.png)有图有真相再难复现的问题也能慢慢揪出来。5. 爬虫的边界与进阶扩展方向5.1 爬虫的合规底线不做“暴力请求机器”写爬虫最大的风险不是技术而是边界感。考试宝这类题库平台的内容是有版权的我做这个项目的前提是只抓未登录可见的公开内容只用于个人学习不批量下载后传播不商用并且严格遵守目标网站的robots.txt和服务条款。在编写代码时也要避免“暴力请求”行为。我做的三个基本操作每次请求之间随机延时1到3秒设置合理的页面访问顺序不做并发并发轰炸一旦发现接口或页面返回明显异常比如验证码弹窗、要求登录立即停止脚本而不是死循环重试。很多人觉得爬虫就是“绕过一切限制拿数据”这是错误的理解。技术上是“能不能”的问题做之前要先考虑“该不该”。如果站点明确禁止爬取不管技术上多简单也应该停手。5.2 扩展点一从全量抓取到增量抓取我最初的版本每次都会从第一页开始抓抓完100页就停止不管这100页是不是重复。但实际使用时题库每天都可能新增题目重复全量抓既费时间又给服务器添麻烦。更好的做法是增量抓取。核心思路是给每条题目计算一个唯一ID可以是题目文本的哈希值抓取前先读取本地已有的题目ID集合解析到新题目时如果ID已存在就跳过每次抓完把新增题目追加到JSON文件里。代码实现也不复杂甚至不需要数据库一个set加一个set就够用。我这里用了哈希值做示例但要注意只对公开内容做数据科学处理不涉及任何绕过机制的意图。5.3 扩展点二任务分隔和并发优化如果抓取量非常大比如几万道题单线程的selenium会显得很慢。但我不建议立刻上手多进程多线程因为selenium并发需要启动多个浏览器实例内存消耗成倍增长。更稳妥的做法是先用单线程把页面列表爬下来只存每道题的详情URL再另写一个脚本用requests或者其他轻量方式去抓详情页。但这里又涉及到一个现象详情页如果是动态渲染的还是得靠浏览器。所以我的实际建议是先评估总页面量如果不超过几千条单线程加显式等待完全够用。真要并发用Playwright的异步API会比selenium并发省资源但那是另一个话题了。5.4 如果不用selenium还有什么路可走写这个项目时我其实也考虑过两条替代路线。一条是用requests模拟浏览器发送请求但关键在于需要逆向接口签名这条路虽然看起来高大上但对新手来说性价比太低。另一条是Playwright它支持更现代的自动化和异步接口启动浏览器也更轻量。不过既然标题就是selenium而且它在国内社区的资料多、问题排查方便用它作为入门框架是最合适的。如果你后续有更大规模的采集需求可以研究一下“先分析接口、再构造请求”的正规做法但前提是取得授权并遵守平台规则。总之技术路线没有绝对的好坏只有适不适合当前的需求。我在实际跑这个项目的过程中最大的体会是selenium爬虫真正难的从来不是拿数据而是让整个流程稳定地重复执行一百次、一千次。考试宝的公开题库页面结构并不复杂复杂的是等待、定位、容错这些细节。给刚起步的朋友一个建议先别急着写全功能代码用浏览器手动把要抓的流程点一遍把每个步骤的元素路径记录下来然后再用selenium照做这样成功率会高很多。最后再分享一个小技巧把每次运行的日志留下来。遇到问题不用去猜直接翻日志看哪一步异常比什么都高效。这个习惯让我减少了很多无效调试时间。本文还有配套的精品资源点击获取