题库制作流水线
把一堆散装考试资料,变成一个能装在 U 盘里随时刷的单文件 HTML 应用。
本技能源自 NSCA-CPT 644 题的实战(2026-08)——那套题从三份来源合并、交叉校验、
到最终跑通回归测试,全流程都走过一遍。下面每一条坑都是真踩过的。
> v1.1.0:根据 SkillHub TRACE 评测(4.6/5)反馈补强——新增零代码路径、多选题支持、
> 异常处理清单。评测短板(命令行依赖过强 / 多选题判定不足 / 异常处理偏弱)已针对性解决。
安全说明
本技能完全本地运行,不联网、不上传、不触碰系统文件。
| 检查项 | 情况 |
|---|---|
| 网络请求 | 无。全部脚本不发起任何网络连接,纯离线运行 |
| 动态执行 | 无。不调用任何动态求值或外部命令执行接口 |
| 递归删除 | 无。不调用任何递归删除接口;覆盖写入一律用 dirs_exist_ok 合并 |
| 文件写入范围 | 仅用户显式指定的输出路径,不写系统目录或技能自身目录 |
| 权限提升 | 无。不调用任何提权操作 |
| 第三方依赖 | 无。仅用 Python 标准库(json / re / os / io / argparse / collections / difflib / shutil) |
目录规范:scripts/ 放可执行脚本,templates/ 放 HTML 模板与示例数据,
与 WorkBuddy 官方子资源约定一致。
零代码路径优先:默认由 Agent 直接完成解析、去重、校验与出卷;
脚本只在用户主动要求命令行接管时才被调用(见下一节)。
两条使用路径
| 路径 | 适合谁 | 用户做什么 | Agent 做什么 |
|---|---|---|---|
| 零代码路径(默认) | 不想装环境、不想碰命令行 | 给资料 → 确认样例 → 验收 HTML | 全链路:提取 → 解析 → 体检 → 去重 → 校验 → 构建 → 测试 → 交付 |
| 命令行路径 | 想自己掌控管线、复用脚本 | 按下面第 1–8 步跑脚本 | 提供 templates/ 脚本与数据契约 |
默认走零代码路径;只有用户明确要脚本、或要自己调解析器时,才切到命令行路径。
零代码路径(5 步)
第 0 步:确认 5 件事
动手前问用户,答案记进本次会话笔记,后面每步都回看:
- 考试名称 + 主办方?
- 题型:单选 / 多选 / 判断 / 填空?各占多少?
- 题量、及格线、有没有图片题或视频题?
- 资料来源:PDF / Word / 网页?带不带答案和解析?
- 有没有配套图片素材?放在哪?
第 1 步:小样本验证(最重要,别跳过)
- 从资料里只取前 5–10 题做提取和解析。
- 用对比表格呈现给用户:原文片段 | 解析出的题干 | 选项 | 答案 | 解析。
- 用户确认无误后才进全量。有错先调解析规则,重新出样例。
第 2 步:全量解析 + 体检
- 按确认后的规则跑全量解析。
- 自动跑
audit_answers.py六项体检:答案越界 / 选项异常 / 解析与答案不符 / 表格图示引用 / 题干截断 / 多选痕迹。 - 需要人工裁决的冲突题单独列出来,不能替用户拍板。
第 3 步:构建 HTML
- 注入模板生成单文件 HTML。
- 含多选题时启用多选模式(见「多选题支持」)。
- 图片素材用相对路径打包到 HTML 同目录的
素材/文件夹。
第 4 步:headless 回归测试
- 用 Chrome headless 验证:首页渲染、答题判定、图片题、表格渲染、笔记联动、进度导出导入、错题本、纠正答案生效。
- 测试不过就修复,不把半成品丢给用户。
第 5 步:交付
- 单文件 HTML(题库内嵌,双击就能用)
素材/文件夹(相对路径,必须跟 HTML 一起走)- 管线脚本备份(方便以后加题重建)
- 使用说明:题库构成、去重结论、已知瑕疵、备份/导入方法
命令行路径
总流程
资料搜集 → 文本提取 → 题目解析 → 体检 → 去重 → 答案交叉校验
→ 图片/表格挂载 → 构建 HTML → headless 回归测试 → 交付
第 0 步:情报先行
动手前先确认清楚,能省掉后面一半返工:
- 认证体系 / 主办方 / 考试结构(题量、时长、及格线、题型、有没有图片题或视频题)
- 官方题源在哪 — 很多认证官网本身就放模拟题,市面上的题库基本都出自官方题,先拿官方的
- 报名条件、费用、证书有效期顺手记进笔记模块,省得以后到处翻
第 1 步:文本提取
- PDF 用
pypdf(任何 Python 环境都行,pip install pypdf) - 旧版
.doc(非 docx)没有靠谱的提取工具,优先找 PDF 或网页版 - 每个文件提取后先看头、中、尾三段样例再决定用不用。垃圾提取(Canvas 渲染、竖排文字、GBK 乱码)直接换源,别硬修
- 网页题目优先
curl拿源码找内嵌 JSON / JS 数组(常见写法const QUESTIONS = [...]),比网页抓取可靠得多 - PDF 文字层不完整时:先试 Chrome
--headless=new渲染;再试从 PDF 抽内嵌图片(pypdf的page.images)
第 2 步:题目解析
用 scripts/parse_questions.py,八大坑它已经内置对策:
| # | 坑 | 对策 |
|---|---|---|
| 1 | 分部各自重编号(每部分从 1 开始) | 按部分切块解析,id 全局递增、pid 保留分部内编号 |
| 2 | 选项字母后没有点号("A xxx") | 点号设为可选:([A-E])(?:\s*[\.、.]\s*\|\s+) |
| 3 | 全角字符(B、.) |
字母类加 A-E,点号类加 . |
| 4 | 多行题干被截断(只留第一行) | 首个选项之前的文字并回题干 |
| 5 | 答案区跟在每个部分后面,不在文末 | 按部分各自定位答案区 |
| 6 | 答案速查表网格("01-05 BBABB") | 逐段展开 (\d+)-(\d+)\s*([A-E]{5}) |
| 7 | \s* 吞掉换行,答案行被上一匹配吃掉 |
行内空白一律用 [ \t]*,禁用 \s* 跨行 |
| 8 | 题干里嵌独立字母("E. A和B" 被拆成幽灵选项) | 按字母去重保首现,超过 5 个选项直接截断 |
解析完先跑体检,别直接往下走:
python scripts/audit_answers.py 题目.json
六项检查:答案越界、选项异常、解析与答案不符、表格图示引用、题干截断、多选痕迹。
有问题回去改正则重解析,别硬着头皮刷。
第 3 步:去重
python scripts/merge_validate.py 主库.json -o 去重后.json
- 完全相同(题干归一化后前 100 字符一致)→ 自动删后留先
- 高度相似(difflib ≥0.85)→ 只提示,不自动删。同题干不同选项很可能是原卷自带的系列题,误删了找不回来
第 4 步:答案交叉校验
python scripts/merge_validate.py 主库.json --ref 参考库.json --conflicts 待裁决.txt
找第二来源(别的译文版本、带答案的文档、搜索快照)做模糊匹配。
脚本比对的是选项文本而不是字母——选项顺序不同不算冲突,这是最容易误报的地方。
结果分三类:一致 / 冲突 / 参考里没有。冲突的逐条人工裁决,看三点:
- 选项顺序是否不同(字母不同但内容相同 → 不是冲突)
- 题干问的是不是同一件事("心理机制" vs "生理机制" 是两题)
- 解析跟答案是否自洽
裁决依据:官方印刷版 > 民间译文;解析自洽 > 答案孤立。结论写进解析加注。
⚠️ 人工改题时用题干文本定位(find() 匹配题干),别用题号——题号会漂,重跑一次就错位。
第 5 步:图片与表格题
图片题:
python scripts/patch_imgs.py 题目.json 素材目录 --asset-dir 素材 --copy-to HTML目录
配套素材常按"部分.题号"命名,文件名即映射。三种匹配规则:pid / part_pid / id。
表格题:别做成图片。用结构化 table 字段,模板会渲染成真 HTML 表格:
"table": {
"caption": "某店 2025 年各季度客诉统计",
"head": ["季度", "订单量", "投诉数", "投诉率"],
"rows": [["Q1", "12,400", "62", "0.50%"]]
}
批量加表格题就写个几行的补丁脚本。找表格题的技巧:正则扫 如下表|如表|见表
加上 \d+\s*(g|bpm|mg/dl|mmHg|kg|lb|%),比只搜"如图"漏得少。
缺图题:源 PDF 图片丢失时,img 字段指向不存在的文件。给题目加
"note": "⚠️ 原题配图缺失……" 字段,模板会在题干上方渲染虚线警示条;
若答案依赖图片(如"演示动作"类),解析里要写明「按官方答案反推」的判断依据。
找这类题:扫题干 如图|图示|下图|演示一项|图片中 但无 img 字段,再核对每个 img 路径文件是否真实存在(audit2.py 已内置这 8 类检查)。
没有配套素材的图题:从 PDF 抽内嵌图(page.images),按页码 + 题文定位归属,
用多模态看图确认再挂上去。
⚠️ 素材用相对路径引用,交付时素材文件夹必须跟 HTML 一起走。
第 5.5 步:选项截断与解析错配修复
PDF 解析最常见的两类「隐性伤」,交付前必查(模板含 fix_option_e.py / fix_explanations.py 可参考改写):
① 选项截断:PDF 续行丢失导致最后一项被吞成半截(如 "E. A 和")。检测启发式:
- 末选项长度 < 其余选项平均长度的 25%
- 末选项以 「的/和/与/在/或/为/是」 等悬空字结尾
- 注意排除误报:如 "E. 以上都不是" 是完整选项
- 修复:回源文件 grep 题干关键词定位原文续行,逐题恢复全文
② 解析与题目错配:低阈值自动补解析时容易挂上别的题的解析。检测:
- 计算解析与题面(题干+选项)的字符 bigram 重叠率,得分低的逐题人工复核
- 修复须对照题干内容和答案逐题重写,不要批量猜
第 6 步:构建单文件 HTML
python scripts/build_html.py 题目.json 我的刷题页.html
构建前自动校验:题库 key 重复、题目 id 重复、答案不在选项内、缺答案——
有问题直接报错,不等你打开页面才发现。
数据契约
题目 JSON 长这样。换一门考试只换这个文件,不用动 HTML:
{
"meta": {
"title": "页面标题",
"subtitle": "副标题",
"lsKey": "my_exam_v1",
"groupLabels": { "basic": "第一部分", "mix": "混合" },
"footnote": "首页底部说明,支持 HTML"
},
"banks": [
{
"key": "bank1",
"name": "第一章 基础",
"group": "basic",
"desc": "6 题",
"questions": [
{
"id": 1, "pid": 1, "part": 1, "partName": "安全常识",
"text": "题干文字",
"options": ["A. 选项一", "B. 选项二"],
"answer": "A",
"explanation": "解析文字",
"img": "素材/1.png",
"table": { "head": ["列1"], "rows": [["值1"]] }
}
]
}
]
}
字段说明:
| 字段 | 必填 | 说明 |
|---|---|---|
id |
是 | 全局唯一,进度按 id 记录 |
pid |
否 | 分部内题号,用于展示和图片匹配 |
part / partName |
否 | 分部编号与名称 |
text |
是 | 题干 |
options |
是 | 选项数组,形如 "A. 内容" |
answer |
是 | 答案字母。单选 "A";多选 "ABD"(见多选题支持) |
multiSelect |
否 | true 显式声明多选题;不写时 answer 长度 >1 自动视为多选 |
explanation |
否 | 解析 |
img |
否 | 图片相对路径 |
table |
否 | 结构化表格,见上一步 |
lsKey 是 localStorage 的键。换一批题就换一个 lsKey,进度才不会串。
应用自带的能力
- 进度
localStorage持久化,答对出列、答错回列 - 刷题 / 背题 / 随机 / 错题重刷 / 只刷标记 / 未做题随机 N 题
- 复习回滚(SRS):错题答对后不是永久清除,而是按 1/3/7/14/30 天阶梯回归复习
(store.prog[k].srs={lvl,due});到期题从首页横幅 / 题库chip / 笔记模块「🔁 到期」进入;
复习答对升一阶(5 轮全过才彻底毕业),答错自动回滚错题本、进度清零。专治"清了错题但其实一知半解" - 纠正答案:做题页右上角「✏️ 纠正答案」手动改判,判定、背题、解析、徽标全走它,随备份导出。
网上查不到官方答案时,这是最可靠的兜底 - 笔记模块按题库统计正确率徽章(低于 70% 标红),一键刷该模块错题
- 键盘
1-5选答案、←→翻题 - 进度导出 / 导入 JSON
第 7 步:headless 回归测试(必做)
HTML 是给真人用的,跑不了就等于没做:
# Windows
"C:/Program Files/Google/Chrome/Application/chrome.exe" --headless=new --dump-dom \
--user-data-dir=<临时目录> "file:///路径/测试页.html"
# macOS
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless=new --dump-dom \
--user-data-dir=$(mktemp -d) "file:///路径/测试页.html"
⚠️ 三条血泪经验:
- 测试脚本必须先写成 .js 文件再注入,别在 shell 里内联——转义会搞坏
- Windows 下
--user-data-dir要用 Windows 路径,别用mktemp给的/tmp - 测试开头先
localStorage.clear(),否则复用 profile 会让进度累加,结果不可重复
测试点参考:首页卡片数、分组标题、开卷答题、图片 src、表格渲染、笔记模块数、
正确率徽章、统计行数、纠正答案生效与撤销。
第 8 步:交付清单
- 单文件 HTML(题库内嵌,双击就能用)
- 素材文件夹(图片,相对路径)
- 管线脚本备份(方便以后加题重建)
- 给用户的说明:题库构成、去重校验结论、已知瑕疵、素材文件夹不能丢
多选题支持
很多考试(考公、医学、建造师、CPA)含多选题。模板已在数据层预留支持。
数据结构
{
"id": 42,
"text": "下列哪些属于有氧训练?",
"options": ["A. 跑步", "B. 游泳", "C. 深蹲", "D. 骑行"],
"answer": "ABD",
"multiSelect": true,
"explanation": "深蹲属于抗阻训练,不选。"
}
answer可以是单字母"B",也可以是多字母"ABD"。multiSelect: true显式声明;不写时answer.length > 1自动视为多选。
解析注意事项
- 答案区写 "AB" / "A,B" / "A和B" / "AB" 都要归一化为
"AB"(注意全角)。 - 多选题选项通常 4–5 个,解析时不能按单选逻辑截断。
audit_answers.py的「多选痕迹」检查会扫出answer长度 >1 的题,确认是有意的多选而非解析粘连。
UI 与判定
- 多选题:选项可多选,底部出现「确认答案」按钮,未确认前不计分。
- 判定:所选集合与
answer完全一致才算正确,多选 / 少选 / 错选均判错(严格模式,默认值)。 - 背题模式:所有正确选项高亮。
- 纠正答案面板:支持多选。
模板改动要点
在 templates/quiz_app_template.html 里:
pick(i)拆成toggleChoice(i)(切换选中态,不判定)+submitAnswer()(提交后统一判定着色)。isMulti(q)判断题型:q.multiSelect === true || String(q.answer).length > 1。effAnswer(q)返回字符串,纠正答案同样走它,保证判定 / 背题 / 解析 / 徽标一致。
> 当前考试不含多选题时可保持原逻辑不变;一旦资料里出现多选,必须启用上述模式。
异常处理清单
每个失败点都要给出用户能看懂的中文提示,绝不把空结果、乱码或 Traceback 直接丢给用户。
| 异常场景 | 给用户看什么 | Agent / 脚本做什么 |
|---|---|---|
| PDF 已加密 | "这份 PDF 有密码,请提供解密版,或安装 qpdf 后执行 qpdf --decrypt in.pdf out.pdf" |
不继续,等用户处理 |
| PDF 是扫描件 | "这份 PDF 是图片扫描件,没有文字层,建议先 OCR 或换文本版资料" | 尝试 Chrome headless 渲染;失败则终止该文件 |
| 提取结果为空 / 乱码 | "XX 文件提取为空(或乱码),可能编码不匹配或源文件损坏,建议换一份" | 记录日志,不进入解析环节 |
| 解析后选项少于 3 个 | "第 X 题解析异常,选项少于 3 个,请检查原文排版" | 写入体检报告,标红待人工处理 |
| 答案字母越界 | "第 X 题答案 'F' 超出选项范围(只有 A–E)" | 标红并阻断构建,不产出 HTML |
| 多来源答案冲突 | "第 X 题两份资料答案不一致(A vs B),请你裁决" | 列出双方证据,等用户决定,不自动选 |
| 疑似重复题 | "发现 X 组高度相似题目,已暂停删除,请确认是否重复" | 相似度 ≥0.85 只提示,不自动删 |
| 末选项疑似截断 | "第 X 题最后一项只有半截文本(如'E. A 和'),已回源文件恢复" | 长度/悬空字启发式扫描,逐题恢复 |
| 解析疑似错配 | "第 X 题解析与题面匹配度过低,已标记人工复核" | bigram 重叠率 < 阈值即标红,不自动替换 |
| 构建校验失败 | "构建失败:XX 项校验未通过(如题库 key 重复),请修正后重试" | 打印具体字段名,不产出半成品 |
| headless 测试未通过 | "回归测试未通过:XX 项失败,正在修复" | 修复后重测,通过才交付 |
| 浏览器进度数据损坏 | "检测到进度数据异常,已尝试用备份恢复;如仍异常请导入进度备份文件" | 读取备份或提示用户导入 JSON |
断点续跑
- 解析、去重、构建三阶段分别落中间 JSON(
题目_raw.json→题目_merged.json→题目_final.json)。 - 任一阶段失败后,从最近的中间文件恢复,不从头重做。
- 命令行路径下每个脚本都是幂等的,可单独重跑。
本技能的边界
- 不会自动购买或爬取付费题库:只处理用户已有的资料。
- 不保证答案 100% 正确:交叉校验后仍可能冲突,最终裁决需要人工确认。
- 不替代教材和课程:题库是辅助工具。
- 扫描件 OCR 质量有限:复杂表格、公式仍需人工核对。
经验法则
- 别迷信"官方"资料:官方 PDF 也有印刷错误、答案表错位、重复收录
- 解析器写完先小样本抽查(头 / 中 / 尾各几题肉眼比对原文),再全量跑
- 每次改解析逻辑都要重跑全管线,脚本保持幂等
- 手工修复用题干文本匹配,不用题号
- 答案核不完很正常——把「纠正答案」按钮用起来,比什么都实在