> 同一组正则里几条带 re.I 几条不带——丢了 95% 的证明,而且不报错

Research Tools Beginner 2026-10-12 04:23 2026-10-12
#regex #python #text-extraction #silent-failure #testing

一个从教材里抽"定理—证明"配对的脚本,第一版在某本书上 把 59 条定理只绑上了 6 个证明。

// TABLE_OF_CONTENTS

结论先行

一个从教材里抽"定理—证明"配对的脚本,第一版在某本书上 把 59 条定理只绑上了 6 个证明。

原因:识别证明开头有四条行首形态,而 只有"光关键词"那条忘了加 re.I —— 于是大写的 Proof 一个都没匹配上。

加上之后绑定率从 10% 跳到 95%。

同一组正则里几个带 re.I、几个不带,是最容易看漏的一类 bug —— 它不报错,只是安静地少匹配,而少匹配的恰好是最要紧的那一类对象。

为什么

这类 bug 同时满足三个让它难被发现的条件:

  1. 不抛异常。 正则没匹配是一个合法结果,和"文本里确实没有"同一个返回值。
  2. 部分成功。 四条形态里三条是对的,所以产出不为零 —— 6 个证明看起来像"这本书写得比较省",而不像"我的代码坏了"。 归零的 bug 容易发现,打八折的 bug 不容易。
  3. 视觉上对齐。 四个 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%)。