状态:已解决(2026-10-15 09:43,修改注释)。 数据取自批量门禁的运行历史、门禁日志的失败信息以及提交历史。 正则表达式对比表是使用 Python 直接运行得出的值。修复正则表达式和元凶排查工作 尚未进行.
目录
一句话总结
| 影响 | 10-15 01:05 ~ 09:43,批量门禁连续 26 次失败。打包在一起的任务从 3 个累积到 19 个,导致没有任何内容被部署 |
| 原因 | 注释中的“268,729 字节”触发了商标检查正则表达式 `\b(… |
| 为什么耗时变长 | 失败测试所更改的文件与名称不匹配,导致元凶排查返回“未知”,循环机制随即重新运行该批次 |
| 已修复 | 将注释改为“约 262KiB”。在下一轮中 19 个任务一同通过 |
| 遗留问题 | 正则边界、元凶排查、重复失败时停止并报警的机制 |
1. 发生了什么
相机记分牌是 TT Lab 的代表性页面。由于首屏在加载时连同暂不需要渲染的录制、编辑和自动计分代码一起请求了,因此我将其拆分为独立区块,并改为延迟加载。我在该修改的头条注释中写下了这样一句话。
// 정적 import 로 268,729 바이트를 받았다(Fast 3G 첫 그리기 4.7초). …
准确记录数字是个好习惯。问题在于该仓库中存在一项测试,用于 防止乒乓球乐园的静态文件中混入特定的用品商标和内部名称
商标列表中包含一个以乒乓球胶皮闻名的数字名称。没错,就是 729 。
BRANDS = re.compile(r"\b(butterfly|tenergy|…|sanwei|729)\b", re.I)
门禁日志中的失败提示非常诚实:
AssertionError: <re.Match object; span=(1518, 1521), match='729'> is not None : …/backend/static/tt/play/cam.js
2. 为什么“268,729”会匹配“729”:单词边界
\b 匹配的不是字符,而是 位置。即一侧是单词字符(\w —— 字母、数字、下划线),另一侧是非单词字符的位置。
,不是单词字符。因此,,和7之间存在边界。9后面的空格也不是单词字符。因此,9与空格之间也是边界。- 结果:在人眼中是“26万 8千 7百 29”的数字,在正则表达式眼中却是“268”和“729”这两个词。
不仅是千位分隔符,小数点(v1.729)或连字符(729-2)也是如此。如果用 \b 包围数字商标,任何包含数字的文章随时都可能中招。
3. 为什么花了 8 个半小时:未能识别元凶的批量门禁
门禁(整体测试)单次运行大约需要 20 分钟。因此,系统不会为每个任务单独运行,而是将多个任务打包在一起进行测试,通过后一同合并。如果失败,则通过“元凶排查”剔除导致失败的任务后重新运行。
元凶排查使用了以下线索:
- 修改了与失败测试名称(
test_tt_play)同名文件(tests/test_tt_play.py,backend/app/tt_play.py)的任务 - 修改了在失败提示中用引号括起来的
.py文件的任务
这一次,这两条线索都失效了。没有人修改过失败的测试文件,且失败提示中出现的文件不是 .py ,而是 cam.js 。
尽管失败提示中 cam.js 的完整路径 就明摆在那里 ,但元凶排查机制并没有去查看它。结果就是:“门禁失败,未能识别元凶”。
随后,外部循环在几分钟后 重新运行了同一批次 。新提交的任务也一同被卷入了该批次。
| 时间 (10-15) | 批次中的任务 | 结果 |
|---|---|---|
| 01:05 ~ 01:46 | 3 | 失败 3 次 |
| 02:06 ~ 02:25 | 8 | 失败 2 次 |
| 02:45 ~ 08:21 | 9 | 失败 18 次 |
| 08:42 | 18 | 失败 |
| 09:03 ~ 09:23 | 19 | 失败 2 次 |
| 09:43 | 19 | 通过 — 19 个任务一同通过 |
全部 26 次运行测试、文件以及 match='729' 都完全相同(核对过所有残存的 26 条门禁日志)。将注释修改为“通过静态 import 获取了约 262KiB”的任务文件在 09:42 被修正,下一轮测试于 09:43 启动并顺利通过(268,729 ÷ 1,024 ≈ 262.4)。
4. 需要修复什么
正则表达式:数字商标应排除紧邻数字的位置
我用 Python 对比了两个正则表达式。
old = re.compile(r"\b(729)\b")
new = re.compile(r"(?<![\d.,])729(?![\d.,])") # 앞뒤가 숫자·점·쉼표면 상표가 아니다
| 文本 | \b729\b |
排除相邻数字 |
|---|---|---|
268,729 바이트 |
命中 | - |
약 262KiB |
- | - |
Friendship 729 러버 |
命中 | 命中 |
729-2 러버 |
命中 | 命中 |
v1.729 |
命中 | - |
0x729 |
- | 命中 |
新的表达式也并非完美(会捕获0x729 )。既然无法仅凭一个正则表达式完美区分“作为商标的 729”与“数字内部的 729”,那就宁可误报,但更为重要的是 在误报发生时能够立即被察觉 。此项修改尚未实施。
元凶排查:查看失败信息中的路径,未知时则进行拆分
- 将失败信息中出现的 所有 文件路径(无论扩展名是什么)与任务修改的文件进行比对。这一次,仅凭这一点就能直接找出元凶。
- 如果仍然无法识别,则将批次对半拆分,仅运行失败的测试(二分查找)。单个测试只需几秒钟即可完成。
循环:当相同的失败重复出现时,应停止并发出通知
如果同一个测试因为相同的原因失败了两次,那么第三次也会失败。届时应停止重试,并将失败信息展示给人工。26 次重试耗费了 8 个半小时,在此期间,18 个无关的任务被迫打包在一起苦苦等待。
这三项改进均尚未实施。这篇文章就是我的待办事项清单。
5. 这次事故带给我们的教训
- 保护性测试可能会因为错误的原因而失败。 商标检查尽职尽责地完成了它的工作。只不过它不知道该如何区分商标“729”与数字“268,729”。
- “未知”不应是结果,而应是警报。 无法识别元凶不意味着“再试一次”,而是意味着“需要人工介入”。
- 批次任务同甘共苦。 批量门禁虽然节省了时间,但一旦无法识别元凶,整个批次就会沦为人质。
蒂蒂寄语。 729 是一款好胶皮,但如果藏在数字里就会带来麻烦!正则表达式中的
\b不把逗号当作边界。而且,如果因为相同的原因摔倒了两次,请在第三次奔跑前先检查鞋带。
参考
所有资料均于 2026-10-16 查阅。
- Python
re文档(\b, 查看前后文(?<!…)·(?!…)): https://docs.python.org/3/library/re.html