正则表达式为什么会卡死?

3 个讲解板块 · 关联 3 款站内工具 · 内容最后核对 2026-10-09

量词套在已量化的分组上,匹配失败时要试的路径随长度翻倍。

直接答案

因为主流的正则引擎是「带回溯」的,遇到失败要回头换一条路重试。当一个模式可以用多种方式去匹配同一段文字时,引擎会挨个尝试这些可能;如果最终整体匹配失败,它必须把所有可能都试一遍才能确定「确实匹配不上」。某些写法下,可能的切分方式数量随输入长度呈指数增长,于是一段短短几十个字符的文本就能让它算几分钟。

典型的危险形状是「量词套在本身已经量化的分组上」,比如 ^(a+)+$:内层加号可以取一个 a、两个 a、三个 a,外层加号再把这些选择任意重复。给一串 a 后面跟一个不匹配的字符,引擎就得枚举所有切分方式才能判定失败。有意思的是,能匹配的输入永远是快的——爆炸只发生在「差一点就匹配」的输入上,而这恰好是攻击者会发来的、测试用例里永远不会出现的字符串。

一个在测试里跑得好好的正则,上了线偶尔会让页面转圈、把服务卡住几秒甚至更久。日志上看不出异常,CPU 却被打满。这类问题有个名字叫灾难性回溯,也叫正则拒绝服务。

它的机制并不神秘,只是反直觉:问题不出在匹配成功的时候,而出在匹配失败的时候。理解这一点,就能写出既正确又不会爆炸的模式。

1危险形状对照

这些写法看起来很自然,但都能让引擎的尝试次数爆炸。

危险写法问题在哪等价的安全写法
^(a+)+$量词套在已量化的分组上^a+$
^(\w+\s?)+$分组能匹配「带空格」或「不带空格」,长句有指数种切分^\w+(?:\s\w+)*$
(a|a)+两个分支能匹配同样的文本合并成一个分支 a
(\d+|\s)+两个分支覆盖的字符集容易重叠用字符类 (\d|\s) 或拆开处理
^.*_.*$点号能跨过分隔符,回退时反复重试换成 [^_]*_.*

识别口诀:凡是量词套量词、或者并列分支能匹配同一段内容,就先假设它危险,再想办法让每段输入只有一种切分方式。

2四条反直觉的事实

这些细节决定了你会不会白费力气。

能匹配就一直很快
爆炸只发生在近似匹配的输入上,所以本地测试用正常数据永远测不出来。
改懒惰量词没用
把加号改成懒惰加号,路径数量一样多,只是尝试顺序反了,照样卡。
加首尾锚点常常更糟
锚点要求引擎「证明没有任何切分可行」,而不是第一个局部失败就放弃,于是把最坏情况逼了出来。
长度限制很有效
指数增长的底数再大,输入被截断到十几个字符时也快得看不出来;先限制输入长度,能挡住绝大多数攻击。

3按优先级修

从「改模式」开始,越往后越像兜底。

手段做法代价
重写模式让各分支互斥、每段输入只有一种匹配方式需要真正理解模式语义,是唯一根治的做法
限制输入长度处理前先截断到业务实际需要的长度极短,几乎没有副作用
换无回溯引擎使用保证线性时间的引擎(如 RE2 系)会失去反向引用等特性
设置匹配超时把匹配放到能被打断的执行单元里,超时即终止只能止损,不能修好
升级依赖排除已知存在此类问题的库版本属于间接防护,仍需自查自有模式

真实世界里这已经造成过大面积故障:有的平台因为一段「去首尾空白」的正则遇上两万个连续空白字符,停机了半个多小时。

相关疑问

为什么我的正则只在某些数据上慢?

因为慢的条件是「匹配失败」,而且输入长得像目标、但结尾差一点。这种输入在正常业务数据里很少见,所以测试时一切正常。用带不匹配尾巴的长字符串测一遍,就能复现。

换成懒惰量词有用吗?

没用。懒惰量词只改变尝试顺序,不减少可能路径的数量,遇到会爆炸的模式一样慢。真正的解法是让模式变得无歧义,或者限制输入长度。

加首尾锚点不是能提高性能吗?

在简单模式上通常有益,但在嵌套量词这类危险形状上反而更差。锚点要求引擎穷举所有切分方式、确认全都失败,才肯判定不匹配,正好把最坏情况逼出来。

怎么提前发现这种问题?

静态检查能标出大部分危险形状:嵌套量词、能匹配同一段内容的重叠分支。把这类检查加入代码审查流程,再配合限制输入长度,就已经挡掉绝大多数风险。

内容标识:本页内容由人工智能辅助生成,并经人工核对与编辑。详见 人工智能生成内容标识说明。
内容说明:本页针对具体使用问题作答,方法步骤与数值由本站自行整理与复算,事实性内容以对应官方规范与文档为准。内容最后核对日期:2026-10-09。若发现错误,欢迎通过页脚反馈入口告知。