科目一题库自动化整合:从网页逆向到Word文档生成
在准备机动车驾驶证考试的过程中,科目一的1685道理论题往往成为考生最头疼的部分。虽然市面上有各类APP和在线练习平台,但多数只能逐题刷题,缺乏集中复习、离线查阅或打印精读的能力。更关键的是,移动端网站通常通过JavaScript动态加载题目,无法直接复制内容。
有没有办法把这1685道题一次性抓下来,整理成一个结构清晰、图文并茂的Word文档?答案是肯定的——只要我们能定位到后台的真实数据接口,并编写一段轻量级爬虫程序。
打开驾驶员考试网(移动端),你会发现页面非常简洁:点击“下一题”就能切换题目,但没有导出功能,也没有明显的API调用痕迹。这时候就需要借助浏览器开发者工具来“透视”前端行为。
按F12进入DevTools,切换到Network → XHR标签页,刷新页面后点击“下一题”,很快就能观察到一条请求路径:
https://tkdata.mnks.cn/ExamData/{code}.json?CALL=?{version}.json这个URL显然不是普通的HTML页面,而是一个返回JSON数据的接口。其中{code}是每道题的唯一编码,{version}是当前题库版本号。进一步查看页面源码或Sources面板中的JS文件,可以发现如下关键变量:
var ExamVersion = "20201231143735"; var ExamCount = 1685; var ExamCodes = "02dec,0d977,02567,...,fcd0b"; // 共1685个逗号分隔的编码这些信息正是我们梦寐以求的核心参数:题数、版本号、全部题目编码列表。有了它们,就等于拿到了整套题库的“钥匙”。
拿到入口之后,先来看看单个题目的响应格式。以一道判断题为例,请求结果如下:
{ "tkId": 2013, "sortId": 1501, "code": "3fd79", "tx": 1, "tm": "驾驶机动车在道路上违反道路通行规定应当接受相应的处罚。", "tv": "", "da": "对", "tags": "违反道路通行规定" }字段含义很直观:
-tm是题目正文;
-da是正确答案(“对”或“错”);
-tv是图片路径,为空表示无图。
而对于选择题,情况稍复杂一些。部分题目的tm字段中包含<br/>分隔符,例如:
"tm": "驾驶人有下列哪种违法行为<br/>一次记6分?<br/>A. 使用其他车辆行驶证<br/>B. 饮酒后驾驶机动车..."这意味着我们需要用.split('<br/>')将其拆分为题干和选项两部分处理。
当tv不为空时,其值形如/SU/od50vqgFW8x,去除前缀后拼接到图片基础域名即可得到完整链接:
img_code = data['tv'].replace('/SU/', '') img_url = f"https://sucimg.itc.cn/sblog/{img_code}"这类图片通常是交通标志、路况示意图等关键视觉信息,必须下载并嵌入最终文档中。
接下来就是动手实现爬虫逻辑。推荐使用 Conda 创建独立环境,避免依赖冲突:
conda create -n driver_exam python=3.8 conda activate driver_exam pip install requests lxml docx beautifulsoup4 urllib3主要依赖说明:
-requests:发起HTTP请求获取JSON数据;
-python-docx:操作Word文档,支持插入文本与图片;
-urllib.request:用于下载网络图片资源。
开发工具建议使用 PyCharm 或 VS Code,调试过程会更加顺畅。
下面是一段可运行的完整代码,用于将全部1685道题抓取并写入Word文档:
import requests import time import urllib.request from docx import Document from docx.shared import Pt from docx.enum.text import WD_PARAGRAPH_ALIGNMENT def download_image(img_url, save_path): try: headers = {'User-Agent': 'Mozilla/5.0'} req = urllib.request.Request(img_url, headers=headers) with urllib.request.urlopen(req) as resp: with open(save_path, 'wb') as f: f.write(resp.read()) return True except Exception as e: print(f"图片下载失败: {e}") return False # 初始化文档 doc = Document() doc.add_heading('机动车驾驶证科目一理论考试题库(共1685题)', level=1) # 配置参数 ExamVersion = "20201231143735" base_url = "https://tkdata.mnks.cn/ExamData/" img_base_url = "https://sucimg.itc.cn/sblog/" image_dir = "./images/" # 本地保存图片目录,请提前创建 import os os.makedirs(image_dir, exist_ok=True) # 完整题目编码列表(此处省略具体字符串,实际使用需补全) ExamCodes = "02dec,0d977,02567,84832,3fd79,..." # 省略中间部分 ExamCodes = ExamCodes.split(',') headers = { 'Referer': 'https://m.jsyks.com/', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } for idx, code in enumerate(ExamCodes): url = f"{base_url}{code}.json?CALL=?{ExamVersion}.json" try: resp = requests.get(url, headers=headers, timeout=15) data = resp.json() lines = data['tm'].split('<br/>') title = lines[0] options = lines[1:] # 添加题目编号与题目 p = doc.add_paragraph() p.add_run(f"{idx+1}、{title} ").bold = True p.add_run(f"[答案:{data['da']}]").italic = True # 添加选项(若有) for opt in options: doc.add_paragraph(f" {opt}", style='List Bullet') # 插入配图(若存在) if data['tv']: img_code = data['tv'].replace('/SU/', '') img_url = f"{img_base_url}{img_code}" img_path = f"{image_dir}{img_code}.jpg" if download_image(img_url, img_path): paragraph = doc.add_paragraph() paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER run = paragraph.add_run("") run.add_picture(img_path, width=Pt(300)) print(f"已处理第 {idx+1}/{len(ExamCodes)} 题") # 控制请求频率,防止被封IP time.sleep(0.5) except Exception as e: print(f"第{idx+1}题抓取异常: {e}") continue # 保存最终文档 output_file = "科目一1685道题库整理版.docx" doc.save(output_file) print(f"\n✅ 所有题目已成功导出至:{output_file}")⚠️ 实际运行前请确保:
- 已将完整的ExamCodes字符串粘贴进代码;
- 手动创建./images/目录用于存放图片;
- 可考虑分批执行(如每次300题),避免长时间运行中断导致重来。
除了主干题目外,部分题目还提供详细解析信息,可通过另一个接口获取:
https://tkdata.mnks.cn/ExamNote/{code}.json?CALL=?{version}.json示例代码如下:
import json for i in range(10): # 示例仅取前10题解析 code = ExamCodes[i] note_url = f"https://tkdata.mnks.cn/ExamNote/{code}.json?CALL=?{ExamVersion}.json" try: res = requests.get(note_url, timeout=10) notes = json.loads(res.text.lstrip('[').rstrip(']')) # 去除非法包裹字符 print(f"\n【第{i+1}题 解析】") for item in notes: print(item['cnt']) except Exception as e: print(f"第{i+1}题无解析或获取失败: {e}")需要注意的是,并非所有题目都有解析内容,部分请求可能返回空数组或404错误。
整个项目虽小,却完整涵盖了现代爬虫开发的关键环节:找接口、提参数、控频率、存数据。对于初学者而言,这是一个极佳的实战案例——它不需要复杂的框架,也不涉及反爬对抗,重点在于理解前端行为与数据流之间的关系。
最终生成的Word文档不仅可用于打印复习,还能导入Notion、Obsidian等知识管理工具进行二次加工。未来还可拓展方向包括:
- 使用pandas导出为Excel表格,便于筛选分类;
- 结合标签字段自动归类(如“交通标志”、“高速驾驶”等);
- 利用pdfplumber对比官方PDF题库验证完整性。
这种“轻量级逆向 + 自动化整合”的思路,在日常工作中极具实用价值。无论是提取企业培训资料、归档历史公告,还是构建私有知识库,都可以依此模式快速实现。
技术的本质,从来都不是炫技,而是让繁琐变得简单,让不可控变得可预期。