PDF 转在线练习题:问题与解法¶
状态: ✅ 已完成
创建日期: 2026-03-05 最后更新: 2026-03-05
背景¶
将 AP World History 的 PDF 试卷(875道选择题,400页)转换为 Markdown 练习文档,过程中遇到大量图片映射、页码偏移、答案提取等问题。本文记录完整的问题清单、根因分析和解决方案,供后续类似 PDF 转练习题场景参考。
源文件¶
| 文件 | 说明 |
|---|---|
| 题目 PDF(400页) | 无答案标记的干净试卷,包含图片和题目文本 |
| 答案 PDF(414页) | 带绿色高亮标记正确答案,排版因高亮背景色膨胀多出14页 |
产出文件¶
| 路径 | 说明 |
|---|---|
02-academic/AP-World-Test/mcq_unit{1-9}_part{a-d}_20260304.md |
23个 Markdown 练习文件,共875题 |
02-academic/AP-World-Test/img/p{N}_fig{M}.png |
163张从题目 PDF 提取的图片 |
scripts/ap_history/question_index.json |
完整索引:每题的答案PDF页码、题目PDF页码、图片、答案、验证状态 |
scripts/ap_history/answer_key.json |
878条从答案 PDF 高亮提取的答案 |
scripts/ap_history/reextract_ap_world_images.py |
8阶段图片提取+映射主脚本 |
scripts/ap_history/fix_ptags_and_verify.py |
页码修正+答案提取+验证+索引构建脚本 |
scripts/ap_history/spot_check_20.py |
20题随机抽查验证脚本 |
问题一:图片大量重复和错误映射¶
现象¶
首次提取得到 275 张图片,但 MD5 去重后只有 143 个唯一哈希。同一张图以不同文件名出现在多个题目中(如 unit1_q12_fig1.png 和 unit1_q13_fig1.png 内容完全相同)。
根因¶
PDF 内部通过 xref(交叉引用)复用图片对象。同一个 xref 被多个页面引用时,按页面逐一提取会得到重复文件。原始脚本按"单元+题号"命名图片,但题号在不同 section 中会重启(如 unit3 和 unit4 都有 Q1),导致命名冲突和错误关联。
解法¶
- 删除全部旧图片,从头提取
- 按 PDF 文件页码命名:
p{页码}_fig{序号}.png(如p67_fig1.png= 题目PDF第67页的第1张图) - 用 PyMuPDF 的
page.get_images()提取,配合 MD5 去重 - 最终得到 163 张唯一图片
关键原则¶
不要使用 PDF 内容中的页码("Page 6 of 10"),直接使用 PDF 文件的绝对页码(1-indexed)。 内容页码在不同 section 重启,会造成混乱。
问题二:图片与题目的对应关系¶
现象¶
图片被映射到了错误的题目。比如一张 Mughal 清真寺照片出现在了一道关于蒙古扩张的题目前面。
根因¶
PDF 中图片的位置规律是:图片下方的题目才是使用该图片的题目。但自动映射脚本有时把图片关联到了上方的题目。
对于多图页面(同一页有两张图,对应不同题目),简单的"最近题目"启发式会出错。
解法¶
- 贪心算法自动映射:对每页的图片,找到其 Y 坐标下方最近的题号
- 对 14 张无法自动匹配的图片,手动指定映射关系(CLOVE 图表、Martellus 地图等特殊案例)
- 修复后运行孤儿检查:确保 0 个未引用图片、0 个断链引用
问题三:P-tag 页码偏移(108题)¶
现象¶
Markdown 中的题目标记 **P68-Q42.** 表示"答案PDF第68页的第42题",但实际查看答案 PDF 发现 Q42 在第 69 页。
根因¶
答案 PDF 因为对正确答案加了绿色背景高亮,排版膨胀(400页→414页)。部分页面上的内容被挤到下一页,导致原始生成脚本记录的页码比实际小 1。
107/108 个错误的偏移量都是 +1,1个是 +2。
排查方法¶
# 从答案 PDF 提取所有题号及其所在页码
for page in answer_pdf:
for match in re.finditer(r'(?:^|\n)\s*(\d+)\.\s*\n', page.text):
qnum = int(match.group(1))
actual_page_map[(page_num, qnum)] = True
# 与 markdown 中的 P-tag 逐一比对
for md_tag in all_markdown_tags:
if actual_page != claimed_page:
mismatches.append(...)
解法¶
逐一将 108 个错误的 P-tag 更新为答案 PDF 的实际页码。
问题四:答案提取——跨页匹配¶
现象¶
从答案 PDF 的绿色高亮提取正确答案时,第一版脚本提取了 752 条(遗漏 123 条)。第二版修复后提取 742 条,但出现 105 个答案与 markdown 不匹配。
根因¶
答案 PDF 的高亮检测面临两个问题:
问题 A:高亮颜色格式
答案行有两种绿色绘图:
- 浅绿背景:fill=(0.902, 1.0, 0.902) — 整行背景色
- 深绿边框:fill=(0.228, 0.568, 0.247) — 字母周围的边框
文本内容是 (D) 而不是单独的 D,正则需要匹配 ^\(([A-E])\) 而非 ^([A-E])$。
问题 B:答案选项跨页
一道题的选项可能从第 N 页延续到第 N+1 页,高亮出现在 N+1 页但题号在 N 页。按页独立处理会把答案分配给 N+1 页上的下一题。
解法:全局 Y 坐标两趟扫描¶
Pass 1: 扫描全部页面,收集两类数据点
- 题号位置: (global_y, page, qnum) # global_y = page_idx * PAGE_HEIGHT + local_y
- 高亮位置: (global_y, page, letter)
Pass 2: 对每个高亮,在全局 Y 轴上找"最近的上方题号"
→ 自然处理跨页情况
最终提取 878 条答案,覆盖全部 875 题。
问题五:答案 PDF 中的"Scoring Guide"¶
现象¶
排查页码偏移时,怀疑答案 PDF 有独立的 "Scoring Guide" 分隔页导致页码错位。
实际情况¶
"Scoring Guide" 是每页右上角的水印标签,414页都有,不是独立分隔页。
答案 PDF 中有 40 个无题号页面(纯图片/stimulus 页),这些才是题目 PDF 与答案 PDF 页码差异的来源之一。另一个原因是高亮背景色导致文本排版膨胀。
验证流程¶
自动验证¶
运行 fix_ptags_and_verify.py:
- Phase 1:从答案 PDF 提取 863 个题号标记
- Phase 2:修正 markdown 中的 P-tag 页码
- Phase 3:用全局 Y 坐标法提取高亮答案
- Phase 4:构建含双页码的 answer_key.json
- Phase 5:逐题比对 markdown 答案与 PDF 高亮
- Phase 6:生成 question_index.json 完整索引
视觉抽查¶
运行 spot_check_20.py:
1. 随机选取 20 道有图片的题目
2. 渲染题目 PDF 对应页为 PNG
3. 对比渲染图与 markdown 中引用的图片是否一致
4. 对比答案是否与 answer_key.json 匹配
最终结果¶
| 指标 | 数值 |
|---|---|
| 总题目数 | 875 |
| 答案全部匹配 | 875/875 (100%) |
| 图片引用 | 162题有图片,0断链,0孤儿 |
| P-tag 修正 | 108个 |
| 答案修正 | 14个 |
关键经验¶
- PDF 页码一律用文件绝对页码,不要用 PDF 内容中的 "Page X of Y"
- 图片命名用页码(
p{page}_fig{n}.png),不要用题号(题号跨 section 重启) - 图片对应规则:图片下方的题目才是匹配的题目
- 答案 PDF 与题目 PDF 页码不同步:高亮/批注会改变排版,导致页面膨胀
- 跨页答案提取:必须用全局坐标匹配,不能按单页独立处理
- PyMuPDF 绘图颜色:
page.get_drawings()返回fill字段为 RGB 元组,需要容差比较(abs(r - target) < 0.02) - 验证必须多维度:自动文本比对 + 视觉抽查 + 索引完整性检查