> 同一组正则里几条带 re.I 几条不带——丢了 95% 的证明,而且不报错
一个从教材里抽"定理—证明"配对的脚本,第一版在某本书上 把 59 条定理只绑上了 6 个证明。
结论先行¶
一个从教材里抽"定理—证明"配对的脚本,第一版在某本书上 把 59 条定理只绑上了 6 个证明。
原因:识别证明开头有四条行首形态,而
只有"光关键词"那条忘了加 re.I —— 于是大写的 Proof 一个都没匹配上。
加上之后绑定率从 10% 跳到 95%。
同一组正则里几个带
re.I、几个不带,是最容易看漏的一类 bug —— 它不报错,只是安静地少匹配,而少匹配的恰好是最要紧的那一类对象。
为什么¶
这类 bug 同时满足三个让它难被发现的条件:
- 不抛异常。 正则没匹配是一个合法结果,和"文本里确实没有"同一个返回值。
- 部分成功。 四条形态里三条是对的,所以产出不为零 —— 6 个证明看起来像"这本书写得比较省",而不像"我的代码坏了"。 归零的 bug 容易发现,打八折的 bug 不容易。
- 视觉上对齐。 四个
re.compile(...)排成一列, 行尾的, re.I在第 3 行有、第 4 行没有, 在 code review 里是最低显著度的差异 —— 比缩进、比变量名都低。
而它的影响不是均匀的:少匹配的那一类(首字母大写的 Proof)
恰好是印刷体最常用的写法,所以损失集中在最主流的那种输入上。
这是启发式规则族的常见形状 —— 覆盖最广的那条规则出错,损失最大,
而剩下的规则会把产出维持在非零,掩盖它。
怎么用¶
把 flag 提到正则族外面,让它无法只对一部分生效:
PROOF_STARTS = [
r'^\s*proof\b',
r'^\s*proof\s*[.:]',
r'^\s*\\begin\{proof\}',
r'^\s*demonstration\b',
]
PROOF_RE = [re.compile(p, re.I) for p in PROOF_STARTS] # 一处定义,全族生效
或者用内联的 (?i) 写进每个模式串里 —— 关键是让 flag 和模式住在一起,
而不是让它成为一个可以单独漏掉的参数。
对规则族做一次 flag 一致性检查。 一行 grep 就能看出不齐:
grep -n 're\.compile' extract.py | grep -v 're\.I' | head
更一般地:同一个列表里的元素,如果有一个带了可选参数而别的没带, 要么是 bug,要么需要一行注释说明为什么。
绑定率/匹配率要作为产出的一部分打出来。 "59 条定理,6 个绑上了证明" 这行数字如果每次都打, 10% 这个值会立刻刺眼。不打的话,它只是产物里少了一些东西。 比值是这类静默缺陷唯一的报警器 —— 同 覆盖率这类比值能抓到不抛异常的缺陷——而"每页小节数"抓不到。
每条规则单独测一次。 规则族的测试很容易写成 "给一段真实文本,断言抽出 N 条",而这种测试对"四条里坏了一条" 是迟钝的。每条形态给一个最小用例(含大小写变体), 坏一条就红一条。
出处¶
~/Tools/booksearch/docs/RAG_PLAN.md §18.3「一个正则标志漏了,
证明就丢了 95%」(59 条定理只绑 6 个证明、四条行首形态里只有
"光关键词"那条缺 re.I、绑定率 10% → 95%)。