约 6 分钟读完 · 更新于 2026-10-21 · 文中每条都能在下面的配套工具里动手验证
贪婪(默认):量词尽量多吃,吃多了往回吐;懒惰(量词后加 ?):尽量少吃,不够再往下吃;灾难回溯:不是新的匹配模式,而是贪婪与懒惰在「多条路都走不通」时的代价——回溯次数随输入长度指数增长,CPU 直接打满。
/".*"/ 匹配 "a" and "b" → 得到 "a" and "b" 整段(从第一个引号吃到最后一个)
/".*?"/ 匹配 "a" and "b" → 得到 "a" 与 "b" 两段
引擎遇到量词会先按贪婪吃满,失败后逐字符往回吐、换一条路再试——每吐一个字符就是一次回溯。正常情况下回溯次数是线性的;但当两个量词能对同一段文字有多种分法(嵌套量词,如 (\d+)+),同一个位置会被「不同的分法」重复尝试,次数就从线性变指数。
识别危险写法的口诀:两组量词嵌套、中间没有明确的分隔符。常见高危:(\w+)+、(\d|\d)+、(.*)*Q。这类正则打在「正常输入」上一切正常,攻击者构造一个「差一点就能匹配」的输入就能触发——这就是 ReDoS 攻击的原理。
/^(\d+)+$/ 匹配 "1111111111111111111111111b" → b 无法匹配,引擎在 25 个 1 的所有分法间回溯 → 约 800 万次尝试,输入再长一位就翻倍
/^\d+$/ 改写一:整体一个量词,没有嵌套 /^(?>\d+)/ 改写二:原子组(占有量词同理),禁止交还已吃字符
抓一份 CPU 采样(或线程栈 dump),栈停在正则执行处即可锁定表达式;然后用 正则测试器复现——喂一段逐步加长的输入,耗时呈平方或指数增长的就是它。修复优先级:消灭嵌套量词 > 加原子组 > 缩小匹配范围。
不一定。懒惰多用于「少匹配」的正确性需求,性能上它同样要回溯(方向相反)。真正的性能差异来自「能否一步到位」:用否定类 [^x] 一步吃到边界,通常比任何量词加懒惰标记都快。
同一思想的两种写法:匹配过的字符绝不交还(禁止回溯到组内)。代价是牺牲一部分匹配能力(某些原本能匹配的输入现在匹配失败),收益是彻底消灭指数回溯。JS 从 ES2018 起支持,Go、PCRE、Java 也都可用。
多数引擎做不到全局关闭,但有等效手段:① 用原子组 / 占有量词包住高危段;② 选不带回溯的引擎(Go 的 RE2、Rust 的 regex crate)跑用户可输入的正则;③ 对外暴露正则功能时加输入长度上限与超时熔断——三层里做任意一层,ReDoS 就打不进来。