A. 痛点描述(Problem)#
正则表达式的“最危险时刻”往往发生在上线之后:
- 某个接口在特定输入下耗时暴涨,CPU 打满
- 日志里只看到一条“请求超时”,你却发现问题出在一个看似无害的正则
- 你只是想校验/提取一点文本,却无意中写出了可被构造输入触发的 ReDoS(正则拒绝服务)
这篇文章专门解决一个问题:如何写出不容易被卡死的正则。
B. 核心原理(Deep Dive)——回溯引擎为何会“指数级爆炸”#
很多语言的正则引擎(包括 JavaScript RegExp)属于“回溯型引擎”:
- 当模式里存在多种匹配路径(例如
a|aa、.*、可选分支、嵌套量词),引擎会尝试一种路径 - 如果后续匹配失败,就回退到上一个分岔点,换另一条路径继续试
当“分岔点数量”与“可回退组合”过多时,尝试次数会指数级增长,最终表现为:一次匹配变成不可接受的耗时。
C. 高风险结构清单(看到就要警惕)#
下面这些结构不一定“必炸”,但在长文本或可控输入下极易出事。
1)嵌套量词(最典型)#
(.+)+(a*)*(.*)+
典型危险例子:^(a+)+$
当输入是很多个 a,末尾再来一个不匹配字符(例如 aaaa...a!),引擎会在“怎么分配每一层 a+”上疯狂回溯。
2)宽泛通配 + 末尾条件很弱#
.*foo.*(bar|baz)
当你用 .* 吞掉大量内容,再在末尾做一个很弱的条件,失败时就会产生大量回溯。
3)分支重叠(多条路径能匹配同一前缀)#
例如:
(a|aa)+(foo|fo)+
当分支之间存在共同前缀时,失败会导致反复在分支之间切换尝试。
4)缺少锚点导致“全局扫描”#
校验类正则如果不写 ^...$,会退化成“在整段文本里不断尝试起点”的扫描匹配,输入越长越慢。
D. 工程优化套路:让正则既快又稳#
1)加锚点:把搜索空间锁住#
如果你的目标是“校验整段输入是否符合结构”,优先写:
^...$
这样引擎不会在每个位置尝试作为起点,大幅减少分支尝试次数。
2)限制量词:把“无限”改成“有上限”#
把 .* / .+ 这种无限量词,改成限定范围:
.{1,200}\w{1,64}[0-9A-F]{32}
这不仅更快,也更符合“工程输入有边界”的现实。
3)避免嵌套量词:改写成单层结构#
把 (something+)+ 这种结构改写为:
- 一层量词 + 更明确的字符类/边界
- 或者拆分两次处理(先粗筛,再精匹配)
例:你要匹配“由多个单词组成的一段文本”,不要写 (\w+\s*)+,更稳的写法是:
^\w+(?:\s+\w+)*$
这类改写通常能显著减少回溯分岔点。
4)先粗筛再精匹配:别让正则承担所有逻辑#
工程里经常可以这样做:
- 先用简单字符串判断(
includes/indexOf/ 长度阈值)过滤掉 90% 不可能命中的输入 - 再用正则对剩余部分做精确提取
这能极大降低“攻击面”(可被构造触发慢匹配的输入范围)。
5)明确“引擎差异”:必要时换 RE2(或做白名单)#
如果你在 Go 里用 regexp,它是 RE2(非回溯),天然不会出现灾难性回溯;但它也不支持部分高级特性(如某些 lookaround)。
如果你的场景需要对外暴露“用户可输入正则”,建议:
- 做长度/复杂度限制(pattern 长度、嵌套层级、量词数量)
- 对高风险结构做检测与提示
- 或者使用 RE2 类引擎以降低 ReDoS 风险
E. 操作指南(Step-by-step)——用小算云箱验证“性能风险”与落地代码#
工具入口:正则表达式测试
👉 立即使用:正则表达式测试
第一步:用“最坏输入”测试你的正则#
不要只用正常样例测试。你应该主动构造:
- 超长文本
- 末尾一个字符导致整体失败(最容易触发回溯)
- 重复结构(例如很多个相同前缀)
如果你发现页面明显变慢,基本说明你的模式存在高风险结构,需要改写。
第二步:检查是否能用锚点/限量词“立刻降风险”#
常见立竿见影的改动:
- 校验类补上
^与$ - 把
.*改成.{0,200}(按你的业务上限) - 把
(.+)+改成更具体的字符类或拆分逻辑
第三步:生成代码并写回归用例#
点“代码生成”导出到目标语言,把“最坏输入”做成测试用例:
- 用例可以防止未来改动把正则再次改坏
- 也能让性能问题更早在 CI 中暴露
F. 常见问题(FAQ)#
1)我只是“提取字段”,也需要考虑 ReDoS 吗?#
只要输入来自外部(用户、日志、第三方系统),都值得考虑。提取类也可能在特定输入下变慢,尤其是用了 .*、嵌套量词、重叠分支时。
2)“懒惰量词”能解决回溯吗?#
能减少一部分“吃太多再回退”的情况,但不是万能药。真正的关键还是:锚点、限量词、避免嵌套量词、减少分支重叠。
3)有什么快速判断“模式是否高风险”的经验?#
看到这些组合就要提高警惕:
- 量词嵌套(
(...+)+、(.*)*) .*后面跟着弱约束或复杂分支- 分支前缀重叠(
(a|aa)、(foo|fo))
工具推荐#
- 正则表达式测试(验证匹配/替换、导出结果、代码生成):立即使用:正则表达式测试
