Java Regex挂在长字符串上

时间:2015-01-12 12:06:31

标签: java regex string

我正在尝试编写一个REGEX来验证字符串。它应该验证它应该只有大写和小写英文字母(a到z,A到Z)(ASCII:65到90,97到122)和/或数字0到9(ASCII:48到57)AND Characters - _~(ASCII:45,95,126)。前提是它们不是第一个或最后一个字符。它也可以有角色。 (点,句号,句号)(ASCII:46)前提是它不是第一个或最后一个字符,并且它也不连续出现两次或多次。我尝试过使用以下

Pattern.compile("^[^\\W_*]+((\\.?[\\w\\~-]+)*\\.?[^\\W_*])*$");

它适用于较小的字符串,但它不适用于长字符串,因为我遇到线程挂起问题和cpu中的巨大峰值。请帮忙。

无效字符串的测试用例:

"aB78."
"aB78..ab"
"aB78,1"
"aB78 abc"
".Abc12"

有效字符串的测试用例:

"abc-def"
"a1b2c~3"
"012_345"

4 个答案:

答案 0 :(得分:3)

你的正则表达式受catastrophic backtracking的影响,导致O(2 n )(即指数)求解时间。

虽然按照链接提供了更全面的解释,但问题是当输入匹配时,引擎会回溯第一个*术语以尝试不同的组合对于术语的数量,但由于所有组或多或少匹配相同的事物,组的方式组合的数量随着回溯的长度呈指数增长 - 在非匹配输入的情况下是整个输入。

解决方案是重写正则表达式,这样就不会灾难性地回溯:

  • 不要使用群组
  • 使用占有量词,例如.*+(永不回溯)
  • 在不匹配时提前失败(例如使用锚定的负面向前看)
  • 使用{n,m}样式量词
  • 限制术语出现的次数

或以其他方式缓解问题

答案 1 :(得分:2)

正如@MarounMaroun已经评论过的那样,你并没有真正的模式。迭代字符串可能更好,如下面的方法:

public static boolean validate(String string) {
    char chars[] = string.toCharArray();

    if (!isSpecial(chars[0]) && !isLetterOrDigit(chars[0]))
        return false;
    if (!isSpecial(chars[chars.length - 1])
            && !isLetterOrDigit(chars[chars.length - 1]))
        return false;
    for (int i = 1; i < chars.length - 1; ++i)
        if (!isPunctiation(chars[i]) && !isLetterOrDigit(chars[i])
                && !isSpecial(chars[i]))
            return false;
    return true;
}

public static boolean isPunctiation(char c) {
    return c == '.' || c == ',';
}

public static boolean isSpecial(char c) {
    return c == '-' || c == '_' || c == '~';
}

public static boolean isLetterOrDigit(char c) {
    return (Character.isDigit(c) || (Character.isLetter(c) && (Character
            .getType(c) == Character.UPPERCASE_LETTER || Character
            .getType(c) == Character.LOWERCASE_LETTER)));
}

测试代码:

public static void main(String[] args) {
    System.out.println(validate("aB78."));
    System.out.println(validate("aB78..ab "));
    System.out.println(validate("abcdef"));
    System.out.println(validate("aB78,1"));
    System.out.println(validate("aB78 abc"));
}

输出:

false
false
true
true
false

答案 2 :(得分:2)

问题

这是由于灾难性的回溯。让我通过将正则表达式简化为匹配原始正则表达式子集的正则表达式来显示它发生的位置:

^[^\W_*]+((\.?[\w\~-]+)*\.?[^\W_*])*$

由于[^\W_*][\w\~-]可以与[a-z]匹配,因此我们将其替换为[a-z]

^[a-z]+((\.?[a-z]+)*\.?[a-z])*$

由于\.?是可选的,我们将其删除:

^[a-z]+(([a-z]+)*[a-z])*$

您可以看到([a-z]+)*,这是导致灾难性回溯(A*)*的正则表达式的经典示例,以及最外层重复(([a-z]+)*[a-z])*可以进一步扩展到([a-z]+)*[a-z]([a-z]+)*[a-z]([a-z]+)*[a-z]的事实加剧了问题(想象一下排列输入字符串的排列数量,以匹配你的正则表达式可以拥有的所有扩展)。这并没有在前面提到[a-z]+,这会增加对伤害的侮辱,因为它的形式为A*A*

解决方案

您可以使用此正则表达式根据您的条件验证字符串:

^(?=[a-zA-Z0-9])[a-zA-Z0-9_~-]++(\.[a-zA-Z0-9_~-]++)*+(?<=[a-zA-Z0-9])$

作为Java字符串文字:

"^(?=[a-zA-Z0-9])[a-zA-Z0-9_~-]++(\\.[a-zA-Z0-9_~-]++)*+(?<=[a-zA-Z0-9])$"

正则表达式的细分:

^                                      # Assert beginning of the string
(?=[a-zA-Z0-9])                        # Must start with alphanumeric, no special
[a-zA-Z0-9_~-]++(\.[a-zA-Z0-9_~-]++)*+
(?<=[a-zA-Z0-9])                       # Must end with alphanumeric, no special
$                                      # Assert end of the string

由于.不能连续出现,并且无法开始或结束字符串,因此我们可以将其视为[a-zA-Z0-9_~-]+字符串之间的分隔符。所以我们可以写:

[a-zA-Z0-9_~-]++(\.[a-zA-Z0-9_~-]++)*+

所有量词都具有占有性,以减少Oracle实现中的堆栈使用并使匹配更快。请注意,在任何地方使用它们都是不合适的。由于我的正则表达式的编写方式,即使没有占有量词,也只有一种方法可以匹配特定的字符串。

速记

由于这是Java并且在默认模式下,您可以将a-zA-Z0-9_缩短为\w并将[a-zA-Z0-9]缩短为[^\W_](尽管第二个对其他程序员来说有点难度阅读):

^(?=[^\W_])[\w~-]++(\.[\w~-]++)*+(?<=[^\W_])$

作为Java字符串文字:

"^(?=[^\\W_])[\\w~-]++(\\.[\\w~-]++)*+(?<=[^\\W_])$"

如果您将正则表达式与String.matches()一起使用,则可以删除锚点^$

答案 3 :(得分:1)

解决方案应该尝试找到否定值,而不是尝试匹配整个字符串上的模式。

Pattern bad = Pattern.compile( "[^-\\W.~]|\\.\\.|^\\.|\\.$" );
for( String str: new String[]{ "aB78.", "aB78..ab", "abcdef",
        "aB78,1", "aB78 abc" } ){
    Matcher mat = bad.matcher( str );
    System.out.println( mat.find() );
}

(值得注意的是,初始语句“string ...应该只有”导致程序员尝试通过在整个长度上解析或匹配有效字符来尝试创建肯定断言,而不是更简单地搜索否定词。)