【问题标题】:From wildcards to regular expressions从通配符到正则表达式
【发布时间】:2012-12-01 12:13:30
【问题描述】:

我想允许两个主要的通配符 ?* 过滤我的数据。

这是我现在的做法(正如我在许多网站上看到的那样):

public boolean contains(String data, String filter) {
    if(data == null || data.isEmpty()) {
        return false;
    }
    String regex = filter.replace(".", "[.]")
                         .replace("?", ".")
                         .replace("*", ".*");
    return Pattern.matches(regex, data);
}

但我们不应该转义所有其他正则表达式特殊字符,如|( 等吗?而且,如果?* 前面有\,也许我们可以保留它们?例如,类似:

filter.replaceAll("([$|\\[\\]{}(),.+^-])", "\\\\$1") // 1. escape regex special chars, but ?, * and \
      .replaceAll("([^\\\\]|^)\\?", "$1.")           // 2. replace any ? that isn't preceded by a \ by .
      .replaceAll("([^\\\\]|^)\\*", "$1.*")          // 3. replace any * that isn't preceded by a \ by .*
      .replaceAll("\\\\([^?*]|$)", "\\\\\\\\$1");    // 4. replace any \ that isn't followed by a ? or a * (possibly due to step 2 and 3) by \\

你怎么看?如果您同意,我是否缺少任何其他正则表达式特殊字符?


编辑#1(在考虑了 dan1111 和 m.buettner 的建议后):

// replace any even number of backslashes by a *
regex = regex.replaceAll("(?<!\\\\)(\\\\\\\\)+(?!\\\\)", "*");
// reduce redundant wildcards that aren't preceded by a \
regex = regex.replaceAll("(?<!\\\\)[?]*[*][*?]+", "*");
// escape regexps special chars, but \, ? and *
regex = regex.replaceAll("([|\\[\\]{}(),.^$+-])", "\\\\$1");
// replace ? that aren't preceded by a \ by .
regex = regex.replaceAll("(?<!\\\\)[?]", ".");
// replace * that aren't preceded by a \ by .*
regex = regex.replaceAll("(?<!\\\\)[*]", ".*");

这个呢?


编辑#2(在考虑了 dan1111 的建议后):

// replace any even number of backslashes by a *
regex = regex.replaceAll("(?<!\\\\)(\\\\\\\\)+(?!\\\\)", "*");
// reduce redundant wildcards that aren't preceded by a \
regex = regex.replaceAll("(?<!\\\\)[?]*[*][*?]+", "*");
// escape regexps special chars (if not already escaped by user), but \, ? and *
regex = regex.replaceAll("(?<!\\\\)([|\\[\\]{}(),.^$+-])", "\\\\$1");
// replace ? that aren't preceded by a \ by .
regex = regex.replaceAll("(?<!\\\\)[?]", ".");
// replace * that aren't preceded by a \ by .*
regex = regex.replaceAll("(?<!\\\\)[*]", ".*");

目标在望?

【问题讨论】:

  • 如果这将出现在公共网站上,那么有人可能会使用它来攻击您的网站。他们可以创建一个永远不会匹配的正则表达式,并且构建为具有大量可能性,因此它将永远运行。然后他们可以用它来淹没你的服务器。
  • 确实,dan1111 是正确的。如需进一步阅读,请查看en.wikipedia.org/wiki/ReDoS
  • @dan1111 你在说哪段代码?如果您在谈论第一个,我同意您的看法,因为用户将能够在过滤器中编写自己的正则表达式。但是第二个的想法正是禁止任何正则表达式特殊字符,并且只允许?* 通配符。
  • @sp00m,即使你只允许.*,这种攻击也是可能的。有关示例,请参见 m.buettner 的回答。基本上,多个相邻的.* 模式会产生大量的匹配可能性,因为有很多方法可以将字符串分解为匹配组。 .*.* 可以通过五种不同的方式匹配abcd('','abcd'), ('a','bcd') 等等。随着更多.* 的添加,这呈指数增长。正则表达式引擎将尝试所有可能性,直到找到匹配项。
  • @dan1111 你说得对,我试图在编辑我的问题时考虑到这一点。你现在怎么看?

标签: java regex string filter wildcard


【解决方案1】:

您不需要在替换字符串中使用 4 个反斜杠来写出一个。两个反斜杠就足够了。

并且您可以通过使用否定的lookbehind来避免替换字符串中的([^\\\\]|^)$1

filter.replaceAll("([$|\\[\\]{}(),.+^-])", "\\$1") // 1. escape regex special chars, but ?, * and \
      .replaceAll("(?<!\\\\)[?]", ".")           // 2. replace any ? that isn't preceded by a \ by .
      .replaceAll("(?<!\\\\)[*]", ".*")          // 3. replace any * that isn't preceded by a \ by .*

我真的不明白你需要最后一步做什么。这不会逃避那些逃避你的元字符的反斜杠(反过来,实际上并没有逃避它们)。我忽略了这样一个事实,即您的替换调用会写出 4 个反斜杠而不是只有两个。但是假设您的原始输入有th|is。那么你的第一个替代品就是th\|is。然后最后一个替换将使 th\\|is 匹配 th-backslash is

您需要区分您的字符串在代码中的外观(未编译,反斜杠数量是两倍)和编译后的外观(仅包含一半的反斜杠)。

您可能还想考虑限制* 的可能数量。像.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*! 这样的正则表达式(在输入中找不到!)可能需要很长时间才能运行。该问题称为catastrophic backtracking

【讨论】:

  • 非常感谢您的建议。我编辑了我的问题,您对此有何看法?
  • 旁注:我的替换字符串中确实需要一个\\\\$1。否则,$1 将不会被视为对第一组正则表达式的引用。
【解决方案2】:

这是我最终采用的解决方案(使用Apache Commons Lang 库):

public static boolean isFiltered(String data, String filter) {
    // no filter: return true
    if (StringUtils.isBlank(filter)) {
        return true;
    }
    // a filter but no data: return false
    else if (StringUtils.isBlank(data)) {
        return false;
    }
    // a filter and a data:
    else {
        // case insensitive
        data = data.toLowerCase();
        filter = filter.toLowerCase();
        // .matches() auto-anchors, so add [*] (i.e. "containing")
        String regex = "*" + filter + "*";
        // replace any pair of backslashes by [*]
        regex = regex.replaceAll("(?<!\\\\)(\\\\\\\\)+(?!\\\\)", "*");
        // minimize unescaped redundant wildcards
        regex = regex.replaceAll("(?<!\\\\)[?]*[*][*?]+", "*");
        // escape unescaped regexps special chars, but [\], [?] and [*]
        regex = regex.replaceAll("(?<!\\\\)([|\\[\\]{}(),.^$+-])", "\\\\$1");
        // replace unescaped [?] by [.]
        regex = regex.replaceAll("(?<!\\\\)[?]", ".");
        // replace unescaped [*] by [.*]
        regex = regex.replaceAll("(?<!\\\\)[*]", ".*");
        // return whether data matches regex or not
        return data.matches(regex);
    }
}

非常感谢@dan1111 和@m.buettner 的宝贵帮助;)

【讨论】:

    【解决方案3】:

    试试这个更简单的版本:

    String regex = Pattern.quote(filter).replace("*", "\\E.*\\Q").replace("?", "\\E.\\Q");
    

    这会用\Q\E 引用整个过滤器,然后停止对*? 的引用,将它们替换为它们的等效模式(.*.

    我用它测试过

    String simplePattern = "ab*g\\Ei\\.lmn?p";
    String data = "abcdefg\\Ei\\.lmnop";
    String quotedPattern = Pattern.quote(simplePattern);
    System.out.println(quotedPattern);
    String regex = quotedPattern.replace("*", "\\E.*\\Q").replace("?", "\\E.\\Q");
    System.out.println(regex);
    System.out.println(data.matches(regex));
    

    输出:

    \Qab*g\E\\E\Qi\.lmn?p\E
    \Qab\E.*\Qg\E\\E\Qi\.lmn\E.\Qp\E
    true
    

    注意这是基于Oracle对Pattern.quote的实现,不知道有没有其他有效的实现。

    【讨论】:

      猜你喜欢
      • 2013-12-16
      • 1970-01-01
      • 2014-06-03
      • 2013-02-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多