学习通题库导入 · XLS 生成(手册 v4)
> 本手册由本项目多轮「生成 → 平台导入 → 报错 → 定位 → 修复 → 再验证」的实测沉淀而成,覆盖 8 档约 1 万道题。
> 所有规则都是平台实测结论,不是推测。凡标注「⚠️」的为会导致导入失败或静默丢题的高危项。
> 版本:手册 v4 ≡ 包版本 v1.2.0(发布包 xuetang-question-bank-import_v1.2.0.zip;frontmatter version 与本文「手册 vN」一一对应,改动一处必须同步另一处)。
> 手册 v4 内容(2026-09-25 深夜,补 A18「删备份前先 grep 谁在读它」+ A19「备份四种定性」,A17 补一次实例;主干:补 A17 清理类「假绿」陷阱;新增:题干出处/版权/水印残留自动清洗 strip_source_tags()、答案文档自动配对比对(verify 第 24 项)、平台题量口径(大题数)、selftest_neg.py 反向自测)
0. 铁律速查(开工前 60 秒读完)
| # | 铁律 | 违反后果 |
|---|---|---|
| 1 | 简单题(单选/简答/填空)的题干写「大题题干」列,「小题题型/小题题干」两列留空 | ⚠️ 整批静默丢弃,平台不报错(2026-09-24 初中 1000 道单选全丢) |
| 2 | 只填自己会填的字段,平台会补的一律不写:选项列不带 A.、题干不带题号 |
显示成 A.A. xxx / 43) xxx |
| 3 | 选项正文前必须补 1 个 NBSP(U+00A0)(填空题/简答题的答案列除外) | 显示成 A.正文(无间距) |
| 4 | 非完型填空的题干里 ## 必须为 0,空位一律写 ______ |
导入卡死在「正在识别 90%」 |
| 5 | 组合题大题行的题型写错、小题题型写 完型填空 |
整组拒收 |
| 6 | 目录列归一(无 /xx英语/ 之类旧前缀残留),全表统一一个根 |
目录被拆成两棵 |
| 7 | 单次导入文件 ≤ 999 题;组合题文件重导前先删平台旧目录 | 超限卡死 / 组合题重复叠加 |
| 8 | 平台只对简单题去重,组合题不去重 → 导入前必须自己做「单元原子级」去重 | 组合题翻倍入库 |
| 9 | 宁缺勿错:答案不可信就整题/整篇剔除,不猜、不编 | 错答案进库 = 教学事故 |
| 10 | 任何新增加工列,必须同步补一条交付前断言 | 漏一条 = 一次上传被拒 |
1. 触发场景与支持的题源类型
1.1 何时调用本 skill
| 触发信号 | 说明 |
|---|---|
用户给出《XX 单元测试题》+《答案》+ importQuestionTemplateNew(题库导入模板).xls,要求生成可上传学习通的题库文件 |
最典型场景 |
| 用户说「把这份试卷做成题库」「导入学习通题库」「按模板填题」「转成超星题库」 | 同义表述 |
用户上传导入失败数据 zip(question_import_error.xlsx)要求排查 |
走第 5 章报错表 |
| 用户报「某题型一题都没导进去」 | 先查铁律 1(简单题布局),再查其余 |
| 已有题库需要按教学档位/层次分批导出分包 | 走 S8 分包 + 目录归一 |
不适用场景(必须先转述给用户):
- 需要音频列——学习通导入模板无音频列,听力题只能导入后在题目编辑页手动挂接。
- 需要图片选项——图片二进制不能塞 XLS,只能写占位并在报告里列「需人工补图」清单。
1.2 支持的题源类型与解析通道
| 题源 | 解析方式 | 关键风险与要点 |
|---|---|---|
| Word (.docx) | python-docx,必须保序读(正文段落与表格交替,用 iter_block_items) |
① 图片选项读不到 → 需 scan_images.py;② 文本框(txbxContent) 常藏选项库/选词库 → 需 dump_textbox.py |
| Word (.doc) | 本机 WPS COM 转换(见用户级记忆《.doc 文件》条目) | 不要走云端转换 |
| Excel (.xls/.xlsx) 已有题库 | xlrd 读 | 最直接——直接当"二级题源",但仍要走第 2 章清洗 |
| 纯文本 / txt | 正则切分 | 空位形态多源不一,需按空位归一规则统一 |
| OCR 图片 / 扫描 PDF | rapidocr_onnxruntime(离线,脚本 jpg_ocr.py) |
OCR 会把行内编号误认、数字粘连(30→3O)、正文印两遍;详见附录 B 大编号题源节 |
| 网页真题 | 直连下载原始 HTML/PDF(_net_fetch_*.py) |
⚠️ 超长汇编页必然被截断;正文取到但答案键被截断 ⇒ 该页不可作答案源 |
| 平台导入失败数据 zip | 解压读 question_import_error.xlsx |
报错不指题号,只能反查本地 |
2. 输入规范:格式要求、预处理与清洗
2.1 题源材料的最低格式要求
| 要求 | 说明 |
|---|---|
| 「题目」与「答案」分离 | 答案文档常按 1)-5) BABCA 紧凑格式给,需展开成 {题号: 答案} |
| 题号可定位 | 每道题要有形如 1. / (1) / 1) 的行首编号(切题正则依赖它) |
| 题干与选项同行或同段 | 若选项跨段散落,需人工归并 |
| 材料编码 UTF-8/GBK 均可 | 读到后统一 html.unescape() 解码 HTML 实体、统一全半角标点 |
| 答案可核验 | 不允许"凭记忆补全"答案;某篇没有可靠答案键就整篇剔除 |
2.2 预处理清洗规则(在切题之前一次性做完)
| 序 | 清洗项 | 规则 | 命中的典型病例 |
|---|---|---|---|
| C1 | HTML 实体解码 | html.unescape(),题干与选项都要 |
don't、she's |
| C2 | 控制字符清除 | 替换 \x01 等控制符为空位 ______ |
历史抓取残留,每格 1–4 个 |
| C3 | 出处标注剥离 | 【[^】]{1,30}】(上限放宽到 30,旧 {1,10} 会漏剥) |
【NMET全国IV-32】、选项尾部 【NMET】 |
| C4 | 题号前缀剥离(只剥行首一层) | ^\s*[\((]?\s*\d{1,3}\s*[\))\..、::]\s*(?![0-9]) |
43) I learned…;(?![0-9]) 用于放行 2.5 million |
| C5 | 选项字母前缀剥离 | 只留正文,去掉 A. / B、 / (A) |
平台会自己补字母,重复补会显示 A.A. |
| C6 | 空位归一 | 只认两类空位:① 连续下划线(_ 与全角 _);② 行内连续半角空格 {2,}(不含制表符、不含行首缩进全角空格) |
版式对齐 satisfactory \t\texp、会话破折号 ----Have you seen 都不是空位 |
| C7 | 多解分隔归一 | which/that、is/was → 统一写 ; |
平台只认 ; |
| C8 | 全半角标点归一 | 逗号 ,→,、冒号→: 等(仅用于匹配键归一化,不改答案内容) |
全角逗号导致匹配失配 |
| C9 | 段落保序 | docx 用 iter_block_items;网页抓取逐段落盘 |
保序是「大题材料 + 小题题干」对齐的前提 |
| C10 | 先落盘原则 | 正文一旦解析出来,无论有没有答案,先存盘(哪怕是 _pending_answer.json) |
实测:没答案就不存,答案补回来时正文也丢了,14 篇只能重抓 |
2.3 答案材料的预处理
官方《答案》文档是给人看的排版,val.parse_answers 只认三种形态:
1-5 BACBC(区间 + 纯字母)1-5 a; b; c(区间 + 分号拆分)1) B 2) C(同行多题)
必须先跑「只改排版、不改内容」的规范化脚本,产出 {KEY}_ans2.txt 再比对。典型病态行与补丁见附录 B 第 8 步表。
两条口径红线:
- XLS 交付用 ;,校验对照文档用空格(val.norm 会把 ; 拆成 2 个单元,口径错位会报「答案序列不一致」)。
- 规范化只做:补题号、拆行、逗号/空格→分号、删行标。任何答案内容改动都是红线。
3. 执行流程(S1–S10,严格按序)
S1 · 核对模板(不可跳过)
读模板第 1 行表头 + 第 2 行说明行,确认 20 列字段与顺序。不同版本模板列数可能不同,不许凭记忆写死。
- 模板第 2 行「导入前请删除此行」必须移除(导入器不会自动跳)。
- 模板自带示例行(如纯文本
作者是赵崇祚)就是「不带字母前缀」的基准,照它写。
S2 · 解析题源
按第 1.2 表选通道。docx 用保序读取;xls 用 xlrd;网页直连下载;OCR 走离线引擎。
> ⚠️ 写校验脚本前先 dump 前 6 行逐列打印(列号 + 表头 + 值)确认真实列映射,再写统计。
> 常见错法:「按 B 列是否为空区分大题/小题」——大题行 A/B 都填、小题行 A/B 都空,判定小题应用 B=='' and E!='';取答案本体用 M 列(不是 G 列)。
S3 · 题目切分
| 场景 | 切分依据 |
|---|---|
| 常规 | 行首题号正则 (?m)^\s*[#\*]*\s*(\d{1,3})\s*[..、] |
| 大编号题源(空位全局连续编号 1..N) | 不要依赖「阅读下面短文…」分隔句(常缺失)。改用「空位编号按每篇空数自然成组」的全局不变量:n % PER == 1 即新篇起点 |
| OCR 编号粘连 | 30→3O、1O 等,容许切题正则前置 #/*;选项清洗时截断污染信号 \s+\d{1,3}\s*[Oo0]\s*\. |
| 官方答案单元数 ≠ 卷面题号数 | 以答案单元数为准重排录入(如 Questions 1–2 (4 points) 实含 4 段对话 → 按 4 题录) |
自检:题号 1..N 连续无缺漏、无重号。
S4 · 题型归一(映射表)
| 原卷板块 | 归一题型 | 结构 |
|---|---|---|
| 听力选择 / 语法选择 / 词汇结构 | 单选题 | 独立题,选项 A–D |
| 完形填空(短文 M 空 × 每空 4 选 1) | 完型填空 | 1 大题行(##1##…##M##)+ M 小题行 |
| 阅读理解(短文 + 选择) | 阅读理解 | 1 大题行(短文)+ N 小题行(小题题型=单选题) |
| 补全对话(N 选项选 M 空) | 共用选项题 | 1 大题行(含共用选项)+ M 小题行 |
| 选词填空 / 段落匹配(A–O 十五词) | 阅读理解型组合题 | 大题 = 整篇(空缺按真题原位保留 26 ______ + 词库原文)+ N 个填空题小题(填词)。⚠️ 材料里绝不能出现 ##n## |
| 选词填空(无词库) | 同上,词库取自官方【词性分析】词表,并在报告写映射表 | 必须在报告注明来源 |
| 语法填空(1 篇 N 空) | 完型填空 / 语法填空组 | 1 大题 + N 填空小题(视平台小题题型支持度选) |
| 听短文填空 / 句子填空 | 填空题 | 1 行,题干含 N 个空,答案 = 字母序列 |
| 翻译(英译汉 / 汉译英段落) | 简答题 | 正确答案 A;选项A = 参考译文;解析 = 采分点;选项数 = 1 |
| 命题作文 | 简答题 | 正确答案 A;选项A = 参考范文;解析 = 评分维度 |
| 「勾选」二选一 / 表格内 √Yes/√No | 填空题(1 空) | 把正确项文本放进选项A |
| A 栏配 B 栏(连线/匹配 5 对) | ⚠️ 模板只有 A–H 八列,5+5 放不下 → 须拆解或改方案 | 不要硬塞、不要丢选项 |
> 遇到模板未覆盖或用户未明确的映射,先问用户,不要自创题型。
S5 · 字段提取与填列
按第 4 章的 20 列表逐个填。要点:
- 独立题(单选/简答/填空):题干 → D 列;E/F 列留空。
- 组合题大题行:题型 → B 列;材料 → D 列;除 A/B/D/I/J/K 外全部留空(含 col2 自定义题型、col4 小题题型必须清空)。
- 组合题小题行:A(目录)与 B(题目类型)留空;填 E 小题题型 / F 小题题干 / G 正确答案 / L 选项数 / M–T 选项。
- 完形填空小题行不填「小题题干」,空位靠大题题干里的
##n##标记。 - 组合题「填空题」小题的「正确答案」必须填选项列字母序列(单空 =
A),答案本体写进「选项A」。
(把真题词库字母G/ 段落字母K直接填进「正确答案」= 引用一个不存在的选项 → 越界。) - 简答/翻译/写作的答案与范文只能写进「选项A」,绝不能写进「答案解析」(会印到学生卷面上)。
S6 · 清洗与布局修复(自动化钩子)
在写文件时依次调用,顺序不可换:
regroup_xuanci → normalize → fix_cloze_markers → fill_cloze_substems
→ fix_simple_layout → strip_source_tags → fix_subjective_answer
→ pad_substem_opts → dedup_units → assert_clean
> ⚠️ 顺序铁律:strip_source_tags 必须在 dedup_units 之前——同题双份入库的副本往往只差一个水印/出处标注,不先清洗就判重会漏掉全部克隆。2026-09-25 实测:清洗前 16 件全绿,清洗后立刻暴露 14 行隐藏重复。
S6-a 简单题布局归位 fix_simple_layout()(2026-09-24 新增,最高优先级)
| 分支 | 判据 | 动作 |
|---|---|---|
| A. 题干归位 | 题目类型==单选题 且 D 列为空/占位(如 单选题)而 F 列有真题干 |
row[3], row[5] = row[5], "" |
| B. 残留小题题型清理 | 题目类型==单选题/简答题 且 E 列非空 |
row[4] = "" |
| C. 英译汉占位归位 | 题目类型==简答题 and D 列 in ("英译汉","翻译") and F 列有值 |
row[3], row[5] = row[5], "" |
> ⚠️ 这是本 skill 最贵的一条经验:学习通规定简单题的题干必须在「大题题干」列;把真题干写在「小题题干」列、D 列写占位,解析器会把它判成「声明了小题却没有小题行」的组合题,于是整行静默丢弃、平台不报错、界面只显示题数变少。2026-09-24 初中两分包共 1000 道单选全部消失,排查数轮才定位。
S6-b 组合题小题题型归一 fill_cloze_substems()
- 小题题型只认『单选题』(学习通支持的词)。写『完型填空』会被判「小题题型为空或不支持」,整组拒收。
- 该钩子对已有值也要强制归一(只补空值会漏掉 540 行同病)。
S6-c 空位标记 fix_cloze_markers()
- 完形大题题干空位写成
##1##…##M##,按下标编号,不用全卷题号。 - 空位来源格式多源不一:半角/全角空格包数字、数字粘词(
not2enough)、编号 16-30 / 46-60 / 61-75。 - 用「独立数字候选的连续递增子序列唯一命中」算法定位,⚠️ 不能简单
find():正文里Oct. 11/45/50/95是真实数字,会被误转。 - 非完型材料里的空位写成真题印刷形态
26 ______/______,不得写##n##。
S6-d 控制字符与非法字符:全表断言 \x01 == 0、'##' == 0(完型除外)、''' == 0、'【' == 0、选项内无 ___ / # / &。
S6-e 目录归一:目录列值统一形如 /高中/单项选择。⚠️ 残留 /高中英语/、/高职英语/ 会生成独立公开目录;/初中//初中/ 双前缀会被拼成怪路径。修源库 + 修生成器,两边都要。
S6-f 出处/版权/水印残留清洗 strip_source_tags()(2026-09-25 新增)
只处理题干两列(大题题干 D / 小题题干 F),不动解析列(G 的【评分维度】【词库】是合法标注)。清洗四类,保留三类:
| 清洗(会印在卷面) | 保留(功能/有意标注) |
|---|---|
| 【版权所有:21教育】、【出处:…】 | 【词库】(选词题词库) |
| 【2024届天津市…一模】、【2019·天津卷】、【2024新课标II卷…】、【…高考真题】 | CET 真题样板套次标注【选词填空/仔细阅读·20XX年·第N套】 |
| 裸水印:21教育网 / 21cnjy.com / www-2-1-cnjy-com / 2-1-c-n-j-y / 21世纪教育网(www.21cnjy.com) / 中小学教育资源及组卷应用平台 | 讲义内容本体里的【导语】【答案】【知识巩固】【段首句】 |
死链接残渣 ( http: / / )、括号版出处(2020高三上·常州期末)等 |
题干内文的真实引用年份(Mike Shepherd(2011)、The Hidden Life of Trees (2015)、裸 (2022)) |
> ⚠️ 实测坑:① 不能用「含【】才处理」做短路——裸水印没有【】;② 括号出处必须带出处关键词(届/期末/一模/联考/中学/卷…)才删,否则会误杀内文引用;③ 水印变体极多(21教育网 / 21cnjy / 2-1-c-n-j-y / www-2-1-cnjy-com / 中小学教育资源及组卷应用平台…),全档扫过一轮后必须再扫一轮确认清零。
S7 · 去重(平台视角,必须自己做)
| 平台行为 | 结论 |
|---|---|
| 简单题 | 平台按「题干+选项」内容自动去重。内容改了会当新题插入 |
| 组合题 | 平台不去重 → 重复导入一次 = 整批新增 |
因此导入前必须做单元原子级去重:
COMBO_TYPES = {"完型填空", "阅读理解", "共用选项题", "选做题"}
def row_sig(row):
"""平台视角指纹:只取平台判重相关的列"""
nrm = lambda x: re.sub(r"\s+", "", str(x)).lower()
cols = [row[1], row[3], row[4], row[5], row[6]]
cols += [row[c] for c in range(12, min(29, len(row)))]
return "\x01".join(nrm(c) for c in cols)
def dedup_units(rows):
"""组合题整单元原子去重;简单题行级去重。返回 (去重后行列表, 丢弃行数)"""
seen_u, seen_r, out, i = set(), set(), [], 0
while i < len(rows):
if str(rows[i][1]).strip() in COMBO_TYPES:
j = i + 1
while j < len(rows) and not str(rows[j][1]).strip():
j += 1 # 扫到下一个大题行为止 = 一个完整单元
ukey = "\x02".join(row_sig(r) for r in rows[i:j])
if ukey not in seen_u:
seen_u.add(ukey); out.extend(rows[i:j])
i = j
else:
h = row_sig(rows[i])
if h not in seen_r:
seen_r.add(h); out.append(rows[i])
i += 1
return out, len(rows) - len(out)
> ⚠️ 行级去重会截断组合题:小题行被 fill_cloze_substems 清空小题题干后,兄弟小题指纹可能碰撞 → 跨组误删,一组只留 2/9/1/6 个小题而不报错。必须整单元判等。
> ⚠️ 指纹不能包含目录/标签/知识点/难度/解析——否则克隆题(仅这些列不同)漏放,被平台过滤掉反而丢题。
补充判重:跨批次并入同一题库时,除 id 判重外再加「正文前缀指纹」re.sub(r"[^a-z]","",p.lower())[:120],并用 difflib.SequenceMatcher(...) > 0.85 做模糊兜底(实测漏检过 Jenifer / Jennifer 差一个字母的同篇)。断言:len({key(e["passage"]) for e in items}) == len(items)。
S8 · 切分包
- 单文件 ≤ 999 题(平台硬限 1000,取 999 留余量)。
- 优先按「目录 / 题型维度」切,好处是各分包目录列一致,导入后自动归并到同一目录树。
- 题量 = 独立题数 + 组合题组数(组合题整组只算 1 题,其下辖小题不单独计数)。
- 交付时主动分成多个文件并逐个标注题数,不给一个超限单文件。
S9 · 写出 .xls(五步原子写,缺一步不得收工)
- 用
xlwt写.xls(与模板同格式);读用xlrd。装包:pip install xlwt -i https://pypi.tuna.tsinghua.edu.cn/simple - sheet 名固定
Sheet1;单元格内换行用\n并设alignment.wrap=1 - 不要合并单元格、不要超链接、不要公式
- 目标目录名:
学习通题库导入分包的命名规范见第 4.4 节
> ⚠️ Workbook() 新建 + save() = 整表覆写,不是「追加」。补源/改库时若直接
> wb = xlwt.Workbook(); ...; wb.save(Path),原库会被整表冲掉,只剩本次写进去的
> 内容,且不报错(2026-09-25 事故:高职阅读真源 100 单元/500 小题被冲成 53 单元)。
> 任何写 bank 真源 / 已有导入件的动作都必须走下面五步(内核已实现,直接调用,
> 不要自己再造一遍:from xls_common import write_atomic; write_atomic(path, rows)):
# S9 标准写入模板:读现有 → 改 → 写临时 → 回读校验 → 原子替换
def write_atomic(path, rows, header, ncol=20):
tmp = path + ".tmp_write"
wb, ws = xlwt.Workbook(encoding="utf-8"), None
ws = wb.add_sheet("Sheet1")
for c, h in enumerate(header):
ws.write(0, c, h)
for ri, row in enumerate(rows, start=1):
for ci, v in enumerate(row):
if v == "" or v is None:
continue # 跳过空值,别写 None
ws.write(ri, ci, v)
wb.save(tmp)
# ① 回读:行数 + 逐格比对,任一对不上立刻删临时文件、放弃写入
sh = xlrd.open_workbook(tmp).sheet_by_index(0)
back = [[sh.cell_value(r, c) for c in range(sh.ncols)] for r in range(1, sh.nrows)]
if len(back) != len(rows) or any(
str(a[c]) != str(b[c])
for a, b in zip(back, rows) for c in range(ncol)):
os.remove(tmp)
raise SystemExit("回读校验失败,已放弃写入 " + path)
# ② 原子替换:避免写一半被打断留下坏文件
os.replace(tmp, path) # 成功 → 删掉 .bak 之外的旧文件
return len(back)
配套三条硬要求:
- 写之前
shutil.copy2(path, path + ".bak_<日期>"),且备份要在改动前做。 - 幂等:脚本能重复跑(已存在则跳过;
drop_clones之类的剔除要「再跑一次剔除 0」),
否则下次重跑会重复追加或重复剔除。 - 改完必须回读复核一次真实结果(单元数 / 小题数 / 关键列),不要相信内存里的
rows。
> 顺带:行序变了要同步重建伴生文件(答案 txt、溯源 md),见 A9。
S10 · 校验(第 5 章)
4. 输出规范
4.1 20 列结构与表头命名
标准表头(顺序不可变,与 importQuestionTemplateNew(题库导入模板).xls 一致):
目录 | 题目类型 | 自定义题型 | 大题题干 | 小题题型 | 小题题干 | 正确答案 | 答案解析
| 难易度 | 知识点 | 标签 | 选项数 | 选项A | 选项B | 选项C | 选项D
| 选项E | 选项F | 选项G | 选项H
> 模板无音频列、无图片列。
| 列 | 索引 | 字段 | 必填 | 填写规范 |
|---|---|---|---|---|
| A | 0 | 目录 | ✅ | 形如 /Unit 1 / /高中/单项选择;仅大题行填,小题行留空;不留旧前缀残留 |
| B | 1 | 题目类型 | ✅ | 仅认可:单选题、多选题、填空题(分录题/资料题)、判断题、简答题(名词解释/论述题)、连线题、排序题、完型填空、阅读理解、共用选项题、选做题 |
| C | 2 | 自定义题型 | — | 仅自定义新题型时填,常规题留空 |
| D | 3 | 大题题干 | ✅ | 独立题的题干写这里;组合题的材料写这里 |
| E | 4 | 小题题型 | — | 仅组合题小题行填,取值受平台支持列表限制(见 4.2) |
| F | 5 | 小题题干 | — | 仅组合题小题行填 |
| G | 6 | 正确答案 | ✅ | 见 4.2 |
| H | 7 | 答案解析 | — | 选填;不可写参考答案/范文(会印到卷面) |
| I | 8 | 难易度 | — | 仅 易/中/难,不填默认中等 |
| J | 9 | 知识点 | — | ⚠️ 仅当课程已存在同名知识点节点才挂载,未命中静默忽略(不像标签会自动新建) |
| K | 10 | 标签 | — | 多个用 ; 隔开;未命中会自动新建一级标签 |
| L | 11 | 选项数 | ✅ | 必须与该行实际填写的选项列数一致,否则报「模板格式错误」;判断题 = 2 |
| M–T | 12–19 | 选项A–H | — | 只填正文,不带 A. / B. / A、 前缀;正文前补 1 个 NBSP(U+00A0) |
4.2 单元格填写规则(高频,逐条对照)
| 题型 | 正确答案列 G | 选项数 L | 选项 A/B 列内容 | 题干空位 |
|---|---|---|---|---|
| 单选题 | 单个字母 A…D |
选项实际个数 | 选项正文 + 前导 NBSP | ______ |
| 多选题 | 升序无重复 ABC |
选项实际个数 | 选项正文 + 前导 NBSP | ______ |
| 判断题 | A(正确)/ B(错误) |
2 |
A=正确、B=错误(都加 NBSP) |
— |
| 填空题(独立) | 字母序列 ABCDE(5 空) |
空数 | A/B/C… = 各空答案本体,不加 NBSP | ______ |
| 填空题(组合题小题) | A(单空) |
1 |
选项A = 该空答案本体,不加 NBSP | 本小题那一句 |
| 简答题(翻译/写作) | A |
1 |
选项A = 参考译文/范文,不加 NBSP | — |
| 完型填空大题行 | 空 | 空 | 空 | ##1##…##M## |
| 完型填空小题行 | 该空正确项字母 | 4 | 四个选项正文 + NBSP | 留空(靠 ##n## 定位) |
| 阅读理解大题/小题 | 同上 | 同上 | 同上 | 小题题干在 F 列 |
通用填充细则
- NBSP(U+00A0):平台渲染选项时按「字母 +
.」与正文直接拼接、不补空格,所以必须自己在正文前补 1 个 NBSP。
- ❌ 普通半角空格:浏览器按 HTML 空白折叠规则吃掉前导空格
- ❌ 全角空格 U+3000:JavaString.strip()视其为空白,导入解析时剥掉
- ✅ NBSP:浏览器不折叠;Character.isWhitespace('\u00a0')为false
- 例外:填空题、简答题的「选项 A–H」列存的是答案/范文本体,补前导空格会污染参考答案、导致判错 → 必须跳过。 - 多空答案字母序列:每位都要有对应选项列;某空多解时在该格用
;分隔。 - 组合题小题行留空属合法(继承大题),校验报告里单独说明,不算空缺项。
- 参考答案/范文只能进「选项A」,解析里放采分点、评分维度、句式得分点。
4.3 字符与编码
| 项 | 要求 |
|---|---|
| 文件编码 | 文件本身是 BIFF 二进制 .xls(与模板同格式),无 BOM 问题 |
| 单元格文本 | 统一 UTF-8 语义读入,写出前清控制字符(\x01 等) |
| 换行 | 单元格内 \n + wrap=1 |
| 禁止 | 合并单元格、超链接、公式、HTML 实体、控制字符 |
| 空格 | 只在「选项正文前」用 NBSP;别处一律普通空格 |
4.4 文件与目录命名
| 对象 | 命名规范 | 示例 |
|---|---|---|
| 单档导入件 | <档位>_学习通题库导入.xls |
高中_学习通题库导入.xls |
| 分包(题数 > 999) | <档位>_学习通题库导入_分包N.xls |
高中_学习通题库导入_分包1.xls |
| 补导缺题型包 | <档位>_学习通题库导入_补导缺题型N题.xls |
本科大一_学习通题库导入_补导缺题型806题.xls |
| 专项补导包 | <档位>_学习通题库导入_<题型>补导<N>组.xls |
高中_学习通题库导入_完形补导16组.xls |
| 8 档合并件 | 分层题库_8档全合并_学习通题库导入_总表.xls + 分包1..N.xls |
— |
> 平台内目录 = 「目录」列的值,建议一个档位只保留一棵目录树;重导前先删平台旧目录(私有 高中 + 公开 高中英语 之类),否则叠加。
5. 校验清单(交付前自检 + 报错修正)
5.1 必跑自检(逐条断言,任一不过不得交付)
| # | 检查项 | 判据 |
|---|---|---|
| 1 | 表头 vs 模板 | 20 列名与顺序完全一致;说明行已删 |
| 2 | 简单题布局 | 题目类型∈{单选题,简答题,填空题} 的行:D 列有真题干、F(小题题干)列必须为空、E 列为空 |
| 3 | 组合题大题行 | 除 A/B/D/I/J/K 外全空(含 C 自定义题型、E 小题题型必须空) |
| 4 | 小题行 | B=='' and E!='' 才算小题;A 列留空 |
| 5 | 选项列前缀 | ^\s*[A-Ha-h]\s*[..、\))::] 零命中 |
| 6 | 题干前缀 | ^\s*[\((]?\s*\d{1,3}\s*[\))\..、::]\s*(?![0-9]) 零命中(D 与 F 两列都查) |
| 7 | 选项间距 | 选择题选项列必须 NBSP 开头;填空题/简答题的选项列必须不带 NBSP。<br>⚠️ **判定代码只能用 ASCII 空白 strip(v.lstrip(" \t\r\n"))——str.strip() 会把 NBSP 一起剥掉,把「已加 NBSP」误判成「没加」,全表假告警(2026-09-25 实测) |
| 8 | 选项数与实填 | 每行 选项数 == 实际填写的选项列数 |
| 9 | 答案合法性 | 字母落在选项范围内;多选升序无重复;单选/多选选项内容两两不同 |
| 10 | 答案与题型匹配 | 单选 1 字母 / 多选升序多字母 / 判断 ∈{A,B} / 填空为字母序列 / 简答=A |
| 11 | ## 计数 |
完型:大题 ##n## 个数 == 本组小题行数;其余题型(单选/简答/填空/阅读…)题干含 ## = FAIL——##n## 只许完形用,其余题型写 ______。⚠️ 2026-09-25 前代码只做了完形组配对、没做这条断言(文档写着「已反向验证」而代码里没有),已补齐 |
| 12 | 非法字符 | \x01 == 0、'&#39;' == 0;选项内无 ___/#/&。<br>⚠️ '【' == 0 只对题干(D/F 列)成立——解析列(G)的【评分维度】【评分档次】【词库】是合法标注,全表扫会产生 900+ 条噪音把真问题埋掉(2026-09-25 实测) |
| 13 | 目录归一 | 全表只有一个根前缀;无 //;无 英语 类旧残留 |
| 14 | 题量上限 | 每个待导入文件平台口径题数(= 独立题数 + 组合题组数)≤ 999,不是数据行数。⚠️ 组合题是「1 大题 + N 小题行」,一行阅读含 10 小题也只算 1 题;实测铁证:高中分包2 数据行 3522 / 平台口径 638 题,导入成功报「638 条」。按行数判会误判成必须切分包 |
| 15 | 完形对应 | 组号连续、空位编号与小题一一对应、无截断组 |
| 16 | 主观题 | 简答类选项A 非空、不带 NBSP;解析里不含参考答案全文 |
| 17 | 去重 | 简单题行指纹唯一、组合题单元指纹唯一(单元原子级) |
| 18 | 答案对照 | 已并入第 24 项(自动配对 + 逐格对齐) |
| 19 | 差异审计 | 重生成后「改前备份 vs 改后」逐单元格比对,非预期差异 = 0,无单元格被误清空 |
| 20 | 题量口径 | 报告写两个口径:数据行数(本地)/ 独立+组数(学习通计费口径) |
| 21 | 答案完整性 | 每道有答案的题都取到了答案;无法识别题型 → FAIL |
| 22 | 答案越界 | 答案字母落在本题选项数范围内 |
| 23 | 组合题小题题干 | 阅读/共用选项/选做的小题行 F 列非空、大题行留空 |
| 24 | 答案逐题比对 | 把答案文档的答案单元序列与 XLS 的序列逐格对齐(difflib,不是按下标硬比)后比对;不一致必须归零。⚠️ 答案文档按「单题库」拆、导入件按「档/分包」切,同名对不上是常态:verify.py 会自动在目录内挑覆盖率最高的那份配对,覆盖率 ≥50% 才判,<50% 只提示「无法配对」 |
> 21~24 已并入 verify.py,与 1~20 一次跑完;val.py 不再单独使用(逻辑已迁入)。
5.2 常见报错与修正办法
| 症状 | 首查 | 修正办法 |
|---|---|---|
| 某题型一题都没导入,平台不报错 | 铁律 1 / 简单题布局 | 题干归位 D 列、清理 E/F 残留列,重建重导 |
| 导入进度条卡在「正在识别 90%」 | 题数 > 1000 / ## 混入非完型 |
先怀疑超限 → 切 ≤999 分包;仍卡 → 全表搜 ##,非完型一律改 ______ |
| 报「模板格式错误」 | 选项数 | 选项数 与该行实际填的选项列数对齐 |
| 报「答案或选项格式不正确」,且不指哪题 | 选项重复 | 下载错误数据反查 → 跑 audit_dupopt.py 定位;按官方材料订正重复项,不得自创;实在无据则删重复项并把选项数改为实际值,列「需教师确认」 |
| 报「小题题型为空或不支持」,整组拒收 | 小题题型取值 | 归一为『单选题』;大题题型也要写对(完形写成阅读理解同样整组拒收) |
导入后显示成 A.A. xxx / 43) xxx |
平台重复补标记 | 选项列去字母前缀、题干去题号前缀,重跑清洗 |
导入后显示成 A.xxx(无间距) |
NBSP | 选项正文前补 1 个 NBSP(填空题/简答题的答案列不加) |
| 目录被拆成两棵 | 目录列残留旧前缀 | 修源库 + 修生成器,归一后重建 |
| 组合题数量翻倍 / 重导后重复 | 平台不对组合题去重 | 重导前先删平台旧目录;导入前做单元原子级去重 |
| 简单题重导被「重复过滤 0 条」 | 正常 | 平台按内容去重,无需处理 |
| 平台题数比本地少 | 计数口径 | 学习通「共 N 题」= 独立题 + 组合题组数;先按此口径复算再查拒收 |
| 题目详情「知识点」为空 | 知识点挂载机制 | 课程里先建同名知识点节点,或改用标签列承载 |
| 听力题无音频 | 模板无音频列 | 导入后在题目编辑页手动挂接音频,报告里列清单 |
| 图片选项为空 | python-docx 读不到 | scan_images.py + extract_optimg.py 导出,XLS 里写「(图片A)」占位 + 报告列「需人工补图」 |
6. 异常兜底与必须确认项
6.1 处理策略(按优先级)
| 情况 | 处置 |
|---|---|
| 官方答案与原卷冲突 | 以官方答案为录入依据,在解析注明,报告「原卷疑点」单列,不改答案 |
| 选项重复且官方材料无法佐证第三项 | 删重复项、选项数改实际值,列「需教师确认」;绝不臆造选项 |
| 某篇没有可靠答案键 | 整篇剔除,不猜不补;另找「单卷」答案源可复用 |
| 官方答案单元数与题号不一致 | 以答案单元数为准重排录入 |
多源答案冲突(A vs D) |
① 先比对选项原文是否一致(不一致 ⇒ 弃非真题那一方);② 一致则拿原文定位句判定,不数源;③ 无法调和 ⇒ 整篇判失败 |
| 正文取到但答案键被截断 | 该页不可作答案源;正文可留用,但需另配答案源 |
| 截取的网页正文不完整 | 直接剔除,绝不「凭记忆补全」重建 |
| 组合卷的音频/图片 | XLS 里占位 + 报告列清单,人工挂接 |
| 无从判断的映射 | 停下来问用户,不自创题型 |
6.2 必须向用户确认的问题(不确认不得自行决定)
- 模板版本与列序(20 列是否一致)
- 未覆盖的题型映射(如连线题、排序题、自定义题型)
- 超 8 个选项(I–O)的选词填空/连线题如何落地
- 音频、图片的挂接方式
- 是否存在历史目录需先删除 / 是否需要先清空平台旧题库
- 「知识点」列是否已在课程里建好同名节点
- 答案与原卷冲突时的取舍
- 某篇无可靠答案键时是「整篇剔除」还是「保留题干待人工补答」
- 难度配比口径(如本科 3:5:2、大二 2:5:3 之类分档要求)
7. 最小示例:输入 → 输出字段映射
7.1 输入素材(节选)
(a) 单选题(原卷排版)
1. The earth _____ around the sun when I was eight.
A. went B. goes C. will go D. has gone
答案 B
(b) 完形填空题(原卷排版)
Complete the passage. Each blank has four choices.
It was a cold morning. She ______ early and lit the stove.
1. A) got up B) get up C) getting up D) gets up → 答案 A
2. A) lights B) light C) lit D) luminous → 答案 C
(c) 英译汉简答题
Part V Translation
1) As you have done the market survey, I'd like to discuss with you the possibility of selling our products in the US.
(无参考答案)
7.2 输出(XLS 前 20 列,索引 0–19)
| 行 | 0 目录 | 1 题目类型 | 2 自定义 | 3 大题题干 | 4 小题题型 | 5 小题题干 | 6 正确答案 | 7 答案解析 | 8 难易度 | 9 知识点 | 10 标签 | 11 选项数 | 12 选项A | 13 选项B | 14 选项C | 15 选项D | …19 选项H |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 表头 | 目录 | 题目类型 | 自定义题型 | 大题题干 | 小题题型 | 小题题干 | 正确答案 | 答案解析 | 难易度 | 知识点 | 标签 | 选项数 | 选项A | 选项B | 选项C | 选项D | 选项E…H |
| 行1 | /初中/单项选择 |
单选题 | (空) | <NBSP>The earth ______ around the sun when I was eight. |
(空) | (空) | B |
(空) | 中 | (空) | (空) | 4 |
<NBSP>went |
<NBSP>goes |
<NBSP>will go |
<NBSP>has gone |
空 |
| 行2 | (空) | (空) | (空) | (空) | 单选题 | (空) | (空) | (空) | (空) | (空) | (空) | (空) | (空) | (空) | (空) | (空) | 空 |
| 行3 | /初中/完形填空 |
完型填空 | (空) | It was a cold morning. She <NBSP>##1##<NBSP> early and <NBSP>##2##<NBSP> the stove. |
(空) | (空) | (空) | (空) | (空) | (空) | (空) | (空) | (空) | (空) | (空) | (空) | 空 |
| 行4 | (空) | (空) | (空) | (空) | 单选题 | (空) | A |
(空) | (空) | (空) | (空) | 4 |
<NBSP>got up |
<NBSP>get up |
<NBSP>getting up |
<NBSP>gets up |
空 |
| 行5 | (空) | (空) | (空) | (空) | 单选题 | (空) | C |
(空) | (空) | (空) | (空) | 4 |
<NBSP>lights |
<NBSP>light |
<NBSP>lit |
<NBSP>luminous |
空 |
| 行6 | /高职/翻译 |
简答题 | (空) | As you have done the market survey, I'd like to discuss with you the possibility of selling our products in the US. |
(空) | (空) | A |
【PRETCO真题·官方参考译文】 |
中 | (空) | (空) | 1 |
你已经作了市场调查,所以我想和你讨论在美国销售我们产品的可能性。 |
空 | 空 | 空 | 空 |
> 说明:
> - <NBSP> = U+00A0。行 1/3 的题干里也加了 NBSP(题干空白占位不影响渲染,但选项列必须有)。
> - 行 2 是干扰行,不应存在——独立题只占 1 行(此题若写成两行即触发「声明小题却无小题行」,会被静默丢弃)。
> - 行 3–5 是完型单元:大题行 ##1##/##2## 与两个小题行一一对应;小题行 A/B 两列留空,答案写在「正确答案」列、选项写在选项列。
> - 行 6 英译汉:正确答案=A、选项A=译文、选项数=1,译文放选项A 而非答案解析(放解析会印到卷面)。
7.3 校验对照
| 检查 | 结果 |
|---|---|
| 简单题布局(行 1、6) | D 列有真题干、E 列空 ✅ |
| 组合题大题行(行 3) | 除 A/B/D 外全空 ✅ |
| 选项前缀 | 零命中 ✅ |
| 题干题号前缀 | 零命中 ✅ |
| NBSP | 选项列有、简答选项A 无 ✅ |
## 计数 |
完型大题 2 个 == 小题行 2 ✅;其余为 0 ✅ |
| 选项数 | 行 1 = 4(实填 A–D)✅;行 6 = 1 ✅ |
| 题量 | 本地 4 题;学习通口径 3 题(独立 2 + 完型 1 组)✅ |
附录 A:规则库索引(历史坑集,已去重编号)
> 以下条目为历次实测沉淀,与正文规则一致但更具体,遇到对应症状时查这里。
A1 · 平台会补什么,你就不写什么(同源于坑 1/2/3)
选项列补字母 → 不写字母;题干补题号 → 不写题号;空格不补 → 自己补 NBSP。最省事的自检是把模板示例行当基准逐字比对。
A2 · 题库题量口径(2026-09-12 B3 全库实测,10/10 零误差)
共 N 题 = 独立题数 + 组合题组数,组合题整组算 1 题。用户报「题数变少」时的诊断顺序:① 先用该口径复算;② 再查是否组合题被拒收;③ 最后去原卷确认分节数差异。不要凭「同一套卷应该一样」回改数据。
A3 · 大编号题源(空位全局连续编号)三条铁律(2026-09-13)
① 篇章切分靠「编号按每篇空数成组」的全局不变量,不靠分隔句;
② 锚定用「逐号顺搜」(?<![\d.\.])N(?![\d]) 跳过正文数字;OCR 把编号变裸下划线时按「裸下划线(≥3) + 紧跟 (word) + gap>1」回补;
③ 渲染只能「按已知坐标替换」,绝不用正则扫全文数字(会把 2025、16th-century、1960s 吞成空位)。
附:Passage N 标题行必须整行剥除;sentence_bounds 需先跨过空位标记再找句末标点;OCR 重复块按「不同编号集合覆盖率 ≥ 70%」裁掉。
A4 · 语法填空编号区间按卷型不同(2026-09-13)
| 卷型 | 编号 | lo/hi/off |
|---|---|---|
| 新高考 I/II、浙江卷 | 56–65 | 56/65/55 |
| 全国甲/乙卷、2014–2020 全国卷 | 61–70 | 61/70/60 |
| 北京卷 2021+ | 11–20 | 11/20/10 |
| 北京卷 2019/2020 | 1–10 | 1/10/0 |
⚠️ 区间逐卷核对原卷,不能按卷种想当然。北京卷 2021 起由 1–10 改为 11–20。
A5 · 公网抓取通道分级(2026-09-13)
| 通道 | 可用性 |
|---|---|
| zy.21cnjy.com/<id> | ✅ 首选(原文+选项+答案+解析一次拿全) |
| mip.21cnjy.com/P/<id>.html、m.51jiaoxi.com/doc-*.html | ✅ |
| bigxun.com、zxxk.com | ✅ 作第二源 |
| renrendoc.com、wenku.baidu.com | ⚠️ 抓不到正文,但搜索片段常带完整答案键 |
| gzyy.cooco.net.cn | ❌ |
大批量补源时拆给多个检索子代理并行(每篇 ≥2 独立源、失败如实报、严禁编造)。
A6 · 答案冲突裁决(2026-09-13 Zafirakou 篇)
汇编类/精校类文档最爱把相邻段落信息安错人安错题。冲突时:① 比对选项原文(不一致 ⇒ 弃非真题那一方);② 一致则拿原文定位句判定;③ 仍无法调和 ⇒ 整篇剔除。
A7 · 双栏 PDF 抽取的交叉核验陷阱(2026-09-15)
用盘上解析 txt 做自动比对前先 re.sub(r"【\s*\d+\s*】", " ", disk);命中率低时按区域 dump 人工逐句核,严禁据自动指标判错。汉译英的盘上答案只有英文译文,不能用中文原文特征串做覆盖检测。
A8 · 规模化抓题必须配独立答案审计代理(2026-09-15 CET-4 Sprint3)
随机抽检发现 1 处真错 ⇒ 全量复核。每个审计代理「先自行求解、不看已记录答案」,再与 ≥2 独立源比对,只输出 {source, stem_head, recorded, verdict, confidence, evidence};low 一律 DROP 而非改字母。定位键统一用「题干 strip() 后前 40 字符」——先截断再 strip 会因换行造成静默失配。反例警示:假订正与漏订正同样有害。
A9 · 伴生文件必须按「XLS 输出行序」生成(2026-09-15)
症状:Gate1 报「答案序列不一致」,肉眼逐题却全对。根因:XLS 按目录/维度分组写出,答案 txt 按取数顺序写 → 整体错位。生成答案文档时直接遍历最终写出的行。
A10 · 难度配比
主观题无难度公式,用分位法(25%/75%)按题量切易/中/难;⚠️ 分数离散时改用「秩次法」(按 (score, 序号) 排序后取秩位切点),否则分位法会落在一堆同分值中间(实测从 23/40/37 修到精确 0.25/0.5/0.25)。
A11 · 自建校验器的坑(两条都实测过)
① 用 _{2,}(?!\s*[((]) 判「该空无提示词」会失效——负向前瞻失败后 _{2,} 回溯到更短匹配使 (?!) 成立,产出大量假告警。正解:两遍法,先收集「带括注」的空号集合,其余即无提示词。
② 判 NBSP 前只能用 ASCII 空白 strip:str.strip() / lstrip() 默认会把 \xa0 当空白剥掉,于是「已正确加 NBSP」被判成「缺 NBSP」,全表假告警(2026-09-25 verify.py 首测即中招)。正解:v.lstrip(" \t\r\n"),空判定用 v.strip(" \t\r\n")。
③ 「跳过后续小题行」必须另用游标 j 从 i+1 起,不能拿已有的 i 做自增循环变量——写成
while i < len(rows) and not rows[i][1].strip(): i += 1 # ← 错:题型非空时立即为 False,i 不前进
会让外层 while i < len(rows) 空转死循环(2026-09-25 实测 verify.py 卡死 3 分钟、faulthandler 才定位到行号)。正确写法是
j = i + 1
while j < len(rows) and not rows[j][1].strip(): j += 1
i = j
A12 · 「已有标签」会误导你以为某卷空白(2026-09-15)
已入库条目可能标注为「全国」「海南」却实为「2020 新高考I 卷 C/B 篇」。抓取整卷前先拉现有 id 清单逐条核对卷别,合并时把疑似重复全打 skip 人工过目,绝不直接覆盖。
A13 · write_xls() 的表头会被写两遍(2026-09-25)
入参既可能是「已含表头」(build() 的返回形态),也可能是「纯数据行」。三处必踩:
① 无条件写表头 + 数据从 enumerate(rows, start=1) → 表头重复、整表行号 +1;
② 只改 start 不改 body → 多出一行空白、表头被推到第 2 行;
③ 判断出「已含表头」却只写数据不写表头 → 表头行全空。
正解:has_hdr = rows[0] == HEADER,body = rows[1:] if has_hdr else rows,
表头行单独写一行(取值优先用原表头),数据 enumerate(body, start=1)。
症状:某文件突然多 1 行、行号整体错位、题量虚高 1,进而误判「1000 > 999 超限」。
A14 · 答案文档与导入件的「同名配对」几乎永远失败(2026-09-25)
答案文档按单题库拆(初中_单项选择_ans.txt 之类),导入件按档/分包切,
名字天然对不上 —— 16 件交付件当初全部报「未配对、跳过比对」,等于第 24 项在真实
交付上从没跑过。且按下标硬比必然报「首个差异 #1」:单库答案在整卷里只是中间一块。
正解:用 difflib.SequenceMatcher 把「题库序列」当 a、「文档序列」当 b 求 opcodes,
只在 replace/insert/delete 里逐格比并带上该单元对应的物理行号,
覆盖率 ≥50% 才判配对成功,≥0.9 的差异才判 FAIL(配对可信),低覆盖只 WARN。
A15 · 「整表覆写」事故:xlwt.Workbook() 新建 + save() 不是追加(2026-09-25)
症状:不报错,但被写的库从 100 单元/500 小题变成 53 单元/318 行,题量凭空少一半,
且「少了的那一批」查无遗迹(内存里的 rows 看着是对的,只有落盘结果不对)。
根因:xlwt.Workbook() 起的是一张空表,save() 直接覆盖目标文件,少了「读旧 + 合并」一步。
正解:五步原子写(读现有 → 改 → 写临时 → 回读校验 → os.replace,模板见 S9),
外加三条配套:.bak_<日期> 备份、脚本幂等、落盘后立刻 xlrd 回读断言单元数/小题数/关键列。
> 同类错误的另一面:只删导入件不动 bank 真源 → 下次重跑生成器原样带回;
> 只删 bank 真源不重跑生成器 → 导入件与真源不一致。二者必须同批落地,
> 并以「真源再跑 drop_clones 剔除 0」验证幂等(高职阅读 / 大三语法填空两轮均照此执行)。
A16 · 删克隆要按「小题集合指纹」,不是「全单元指纹」(2026-09-25)
克隆对只在篇章/解析/篇号上有差异 → 用「小题题干+答案+选项」排序后的集合指纹判重
(_sub_fp),而不是整单元拼接指纹。顺序铁律:先 drop_clones(),再 dedup_units(),
反了会被行级去重截成半截组合题。判完必须人工抽验 8–10 篇,确认每篇小题数与空位数一致。
A17 · 清理类操作的「假绿」陷阱:命中数 vs 归档条目数必须双印证(2026-09-25)
批量删 .log / .bak 类脏数据时,若「扫描清单」与「执行脚本」相隔数分钟(跨工具调用很常见),
目标可能已被外部删除(手工清理 / 同步盘 / 其它进程),脚本会静默命中 0 并在报告里写成已清理。
两条硬要求:① 归档 zip 的条目数与命中数必须一致——zipfile.ZipFile(z).namelist() 自查;
② 执行前再 os.path.exists() 复测一次总数。任一项不符即如实上报「该批未被本轮删除」,
不得含糊写成「已清理」。另:匹配备份别只写 .bak_20260925,…_xls.bak_item17_20260925
这类前后缀变体要单独列,否则会漏。
再次命中(09-25 夜间):dry-run 报「可删 56 个」,执行时归档只收进 38 条,差 18 个。
这一批同样是被外部删的。凡 dry-run 数与归档条目数不等,一律按「有文件在本轮之外消失」上报,
不要解释成"预估误差"。
A18 · 删备份前先 grep「谁在读取它」(2026-09-25)
.bak 不一定只读不写——有些脚本把它当输入基表。09-25 实测:bank_quota.json.bak_20260920_大二入账
被 _work/_quota_fix_daer.py 读取、…_p3b 被 patch_ledger_p3b.py 读取、…_p6_junior 被
patch_ledger_p6.py/p6b.py 读取。删了不会报错,只会让这些脚本下次崩在文件打开上。
删除任何 .bak 前必做:grep -rn "\.bak" --include="*.py" 全库扫一遍,命中的一律进保留清单。
A19 · 备份的四种定性(决定删/留,别只看后缀)(2026-09-25)
| 类别 | 判据 | 处置 |
|---|---|---|
| A1 冗余 | 与同族正式件 md5 相同 | 删 |
| A2 中间态 | 同族正式件存在、内容不同("改动前快照") | 删(先归档) |
| B 唯一副本 | 同目录 + 3 层父目录均找不到同族正式件 | 留 |
| C 脚本依赖 | grep 到被 .py 读取 |
留 |
判定脚本自身两个反直觉陷阱(都真实踩过,会直接误判):
① os.path.splitext('x.json.bak_20260920') 拿不到基名 → 所有条目被当成"无同族正式件"(误判 B);
改用 split 法:name.split('.bak')[0] 之类逐令牌剥离。
② 修好基名后又出现自匹配——怪名文件把自己的变形当成同族正式件,判成 A1 冗余被误删;
必须显式排除自身路径:if os.path.abspath(sib) == os.path.abspath(p): sib = None。
③ 易误判为 B 的一类:_work/backup_20260924/*.xls("前目录归一"快照,正式件已迁移到别处)——
这类是真唯一副本,必须留。
参考脚本(本 skill 的 scripts/ 目录)
按「先取素材 → 再构建 → 后校验 → 出报告」的顺序使用。
| 脚本 | 作用 |
|---|---|
lib.py |
推荐主库:Bank 类(mc / fill / shared / shared_sub / cloze / reading / judge / R / save)。R() 自动对 D/F 跑 clean_stem()、对选项列跑 clean_opt() + pad_opt() |
build_template.py |
build 骨架(clean_opt() + R() 行构造器 + xlwt 写出) |
validate_template.py |
模板感知校验器 |
val.py |
答案逐题比对器(同行多题、T/F→A/B、简答跳过)。已并入 verify.py 第 24 项,单独跑仅为回溯旧报告用 |
norm_ans.py |
答案文档规范化模板,产出 {KEY}_ans2.txt(见第 2.3 节) |
detail.py |
保序导出「段落 + 图片名 + 表格单元格(空位标 [___])」;图片同时解 DrawingML 与 VML |
check.py |
单份校验入口:python check.py "<名称>" <xls> <答案txt> |
xls_common.py |
规则内核:clean_stem / clean_opt / pad_opt / strip_ctrl / normalize_dir / fix_simple_layout / fill_cloze_substems / fix_cloze_markers / strip_source_tags / fix_subjective_answer / pad_substem_opts / row_sig / dedup_units / assert_clean / build() / write_xls()。第 3 章 S6 的代码就在这儿,直接 import |
verify.py |
第 5.1 节 1~23 项自检 + 第 24 项答案逐题比对的脚本化实现,一条命令跑完全流程:python verify.py a.xls b.xls / --dir <目录> / --all <根目录>(自动收目录内所有 *_ans.txt 做配对)。退出码 0/1。已用 4 个坏样本反向验证:简单题布局错位、选项带字母前缀、题干带题号、非完型含 ## 全部能抓到 |
demo_minimal.py |
第 7 章最小示例的生成器,跑一次即得可对照的 5 行 XLS |
verify_opt.py |
列自检(选项零字母前缀 / 题干零题号前缀 / NBSP 间距 / 选项数一致 / 答案范围 / 题型标准值) |
audit_dupopt.py |
全量体检已产出 .xls:选项重复 / 选项数不符 / 答案越界 / 字母前缀 / 题干前缀(导入被拒时先跑它定位是哪一题) |
run_all.py + spec_all.py |
批量复跑全部登记文件(改动 val.py/lib.py 后必跑,防回归) |
selftest_neg.py |
反向自测:造 5 类坏样本(选项字母前缀 / 简单题布局反写 / 题干带题号 / 非完型残留 ## / 答案越界)逐条验证 verify.py 能抓到。改了 verify.py 必跑,退出码非 0 = 有漏检;临时文件自动清理 |
strip_prefix.py |
已生成 xls 的就地修复(扣选项列误写的字母前缀) |
scan_stemnum.py |
列出题干仍带题号前缀的行(D/F 两列 + 行号 + 原文) |
diff_prestem.py / diff_prepad.py / diff_dupopt.py |
「改前备份 vs 改后」逐单元格差异审计(确认改动只落在预期列) |
scan_images.py |
扫描 docx 内图片/文本框数量,判断有无「图片选项」 |
dump_textbox.py |
提取 docx 文本框(txbxContent) 内的文字——python-docx 读不到,选项库常藏这里 |
extract_optimg.py |
按文档顺序导出图片选项,命名 <卷>_Q<题号>_<选项>.png |
收工前 checklist(照抄执行)
- 自检清单 1~24 项全部通过,输出报告。
- 逐份清点题量(本地口径 + 学习通口径并列),与官方答案键比对「0 错 0 警」。
- 报告固定包含三块:① 逐份清单;② 需人工补全(音频 / 图片挂接);③ 原卷疑点与异常题目。
- 明确告知用户:图片/音频需在学习通题目编辑页手动挂接;知识点列需课程里先建同名节点。
- 提醒用户:重导前先删平台旧目录;组合题文件不可重复导入(平台不去重)。
- 凡写过 XLS 的,落盘后回读复核一次(行数 / 单元数 / 关键列),并把备份路径写进报告;
改过行序的,同步重建答案 txt 等伴生文件。