我们的用户名验证规则如下:
我们有相同的正则表达式:
^(([a-zA-Z0-9]+[_-]*[a-zA-Z0-9]*)([\\.]*[a-zA-Z0-9])*)+$
现在尝试匹配特定的字符串,CPU使用率呈指数级增长。例如:
M45766235H.M96312865E@EXAMPLE.COM
显然,匹配上面的字符串应该会立即失败,但我想知道它为什么需要那么多的CPU周期。最终代码:
import java.util.regex.*;
public class R {
static final Pattern namePattern = Pattern.compile("^(([a-zA-Z0-9]+[_-]*[a-zA-Z0-9]*)([\\.]*[a-zA-Z0-9])*)+$");
public static void main(String... args) {
final String userName = "M45766235H.M96312865E@EXAMPLE.COM";
Matcher matcher = namePattern.matcher(userName);
System.out.println(matcher.matches());
}
}
在不同的方面,我重写了下面的正则表达式并且很好地展示了它:
^[\\w]+[\\w-_\\.]*[\\w]+$
答案 0 :(得分:5)
当正则表达式引擎大量使用回溯时,匹配过程变得非常缓慢。当你让表达式的不同部分与输入的重叠部分匹配时,回溯会发生很多,特别是当没有匹配时:引擎会尝试在正则表达式的各个部分之间分割输入的不同可能性。
从正则表达式中考虑这个简单示例:让我们使用[a-zA-Z0-9]+[_-]*[a-zA-Z0-9]*
来匹配M45766235H.
请注意,有两个子表达式可以覆盖从第二个开始的所有字符:引擎必须考虑所有这些可能性:
[a-zA-Z0-9]+ [a-zA-Z0-9]*
------------ ------------
M45766235H <nothing>
M45766235 H
M4576623 5H
M457662 35H
M45766 235H
M4576 6235H
M457 66235H
M45 766235H
M4 5766235H
M 45766235H
鉴于没有匹配,那就是十次无用的重复。但那不是它的结束!当混合中存在其他可能产生模糊覆盖的子表达式时,将对字符串中的每个可能匹配尝试这十个可能的匹配。
要说回溯的影响很快就会轻描淡写:它们会以几何级数增加,最终耗费大量的CPU。
这个故事的寓意是试图减少回溯量。例如,你的第二个表达
^[\\w]+[\\w-_\\.]*[\\w]+$
可以像这样重写:
^\\w[\\w-_\\.]*\\w$
这两个表达式将匹配相同的输入集,但是当存在匹配时,第二个表达式将具有唯一匹配,而原始表达式将具有大约(N-2)^3
种不同的方式来分割匹配的字符串三个与单词字符匹配的子表达式。
以下是对相关主题的更多阅读:Performance of Greedy vs. Lazy Quantifiers。