【问题标题】:Regex for a username increases CPU consumption用户名的正则表达式会增加 CPU 消耗
【发布时间】:2013-04-29 20:16:41
【问题描述】:

我们的用户名验证有以下规则:

  • 用户名可以包含字母数字字符
  • 用户名可以有下划线、连字符或句点
  • 现在假设用户名是 ASCII 格式
  • 用户名不能以句点开头或结尾
  • 用户名不能开始、结束或有空格

我们有以下相同的正则表达式:

^(([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]+$

【问题讨论】:

  • 这可能是由于一个或多个量词 (+) 封装了整个正则表达式,导致回溯导致您观察到大量 CPU 周期。
  • 这听起来像是catastrophic backtracking...
  • @Vulcan - 感谢您带来全局 (+) 量词,但它是否也取决于用户名的长度?像 username@ 一样简单会立即失败。

标签: java regex


【解决方案1】:

当正则表达式引擎大量使用回溯时,匹配过程变得非常缓慢。当您让表达式的不同部分匹配输入的重叠部分时,会发生很多回溯,尤其是当没有匹配时:引擎会尝试在正则表达式的各个部分之间拆分输入的不同可能性。

考虑一下您的正则表达式中的这个简单示例:让我们使用[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

【讨论】:

  • 感谢您的解释。完全有道理。
猜你喜欢
  • 2010-09-06
  • 2011-02-15
  • 2011-07-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多