【问题标题】:Regex for multiline string literals produces `StackOverflowError`多行字符串文字的正则表达式产生 `StackOverflowError`
【发布时间】:2018-06-05 22:57:25
【问题描述】:

我想匹配包含在三重 "-quotes 中的字符串,这些字符串可能包含换行符,并且除了开头和结尾之外不包含任何 """-substrings。

有效示例:

"""foo
bar "baz" blah"""

无效示例:

"""foo bar """ baz"""

我尝试使用以下正则表达式(作为 Java String 文字):

"(?m)\"\"\"(?:[^\"]|(?:\"[^\"])|(?:\"\"[^\"]))*\"\"\""

它似乎适用于简短的例子。但是,在较长的示例中,例如在包含数千行 hello world 的字符串上,它会给我一个 StackOverflowError

Scala sn-p 重现错误

import java.util.regex.{Pattern, Matcher}

val text = "\"" * 3 + "hello world \n" * 1000 + "\"" * 3
val p = Pattern.compile("(?m)\"\"\"(?:[^\"]|(?:\"[^\"])|(?:\"\"[^\"]))*\"\"\"")
println(p.matcher("\"\"\" foo bar baz \n baz bar foo \"\"\"").lookingAt())
println(p.matcher(text).lookingAt())

(注意:本地测试,Scastie 超时;或者可能将 1000 减少到更小的数字?)。

产生相同错误的 Java sn-p

import java.util.regex.Pattern;
import java.util.regex.Matcher;

class RegexOverflowMain {
  public static void main(String[] args) {
    StringBuilder bldr = new StringBuilder();
    bldr.append("\"\"\"");
    for (int i = 0; i < 1000; i++) {
      bldr.append("hello world \n");
    }
    bldr.append("\"\"\"");
    String text = bldr.toString();
    Pattern p = Pattern.compile("(?m)\"\"\"(?:[^\"]|(?:\"[^\"])|(?:\"\"[^\"]))*\"\"\"");
    System.out.println(p.matcher("\"\"\" foo bar baz \n baz bar foo \"\"\"").lookingAt());
    System.out.println(p.matcher(text).lookingAt());
  }
}

问题

知道如何使这个“堆栈安全”,即有人能找到一个接受相同语言的正则表达式,但在提供给 Java 正则表达式 API 时不会产生 StackOverflowError

我不在乎解决方案是使用 Scala 还是 Java(或其他),只要使用相同的底层 Java 正则表达式库即可。

【问题讨论】:

  • 这里看起来确实有些用处:why (A|B)* results in recursive calls。但是请注意,例如 "(?m)\"\"\"(?:\"?\"?[^\"])*\"\"\"" 会遇到相同的错误,即使它不包含替代品 |
  • """ 必须在字符串的开头和结尾吗?为什么不手动编写呢?它只需要多几行,而且速度会更快
  • @x4rf41 是的,三个双引号,后面跟着任何字符,只要没有三双引号,最后还是三个双引号。我希望有一个这样做的正则表达式,因为它很适合现有的框架。否则,我将不得不发明一些“通用的常规语言接受接口”——如果可能的话,这是我想要避免的。
  • ... 也许我应该明确排除像 """"""" (七个双引号)这样的极端情况,表示带有单个双引号的字符串文字。假设字符串中的所有双引号都与开头的""" 和结尾的""" 用一些其他字符分隔。我不在乎那些奇怪的极端情况是被接受还是被拒绝。
  • 试试这个^"""((?!""")[\s\S])*"""$(没有多行标志)。它在这里工作:regex101.com 与您的两个示例都符合预期。它使用负面的前瞻性来代替。这应该可以防止我希望的堆栈溢出(虽然不确定)。转义:^\"\"\"((?!\"\"\")[\\s\\S])*\"\"\"$

标签: java regex scala stack-overflow


【解决方案1】:

下面的完整答案优化了正则表达式的性能,但为了防止堆栈溢出,作为一个简单的解决方案,只需使重复组占有

具有选择的非占有重复组需要递归调用以允许回溯。使其具有所有格可以解决问题,因此只需在* 之后添加+

"(?m)\"\"\"(?:[^\"]|(?:\"[^\"])|(?:\"\"[^\"]))*+\"\"\""


还要注意,如果你想匹配整个输入,你需要调用matches(),而不是lookingAt()


性能提升

注意:快速性能测试表明这比 answer by x4rf41 中的正则表达式快 6 倍以上

而不是匹配一个

  • 不是报价:[^\"]
  • 正好有一个报价:(?:\"[^\"])
  • 正好两个引号:(?:\"\"[^\"])

在一个循环中,首先将所有内容匹配到一个引号。如果那是单引号或双引号,但不是三引号,则匹配 1-2 个引号,然后所有内容直到下一个引号,根据需要重复。最后匹配结尾的三引号。

匹配是确定的,所以使重复具有所有格。如果输入包含许多嵌入式引号,这也可以防止堆栈溢出。

"{3}          match 3 leading quotes
[^"]*+        match as many non-quotes as possible (if any) {possesive}
(?:           start optional repeating group
   "{1,2}       match 1-2 quotes
   [^"]++       match one or more non-quotes (at least one) {possesive}
)*+           end optional repeating group                  {possesive}
"{3}          match 3 trailing quotes

由于你不用^$,所以不需要(?m)MULTILINE

作为Java字符串:

"\"{3}[^\"]*+(?:\"{1,2}[^\"]++)*+\"{3}"

【讨论】:

    【解决方案2】:

    使用负前瞻的解决方案基本上找到以""" 开头并以""" 结尾并且包含不包含""" 的内容的字符串

    作为普通正则表达式:^"""((?!""")[\s\S])*"""$

    Java 转义正则表达式:"^\"\"\"((?!\"\"\")[\\s\\S])*\"\"\"$"

    \s\S 包括换行符(基本上是. + 换行符或. 带有单行标志)

    这应该在没有多行标志的情况下使用,以便^$ 匹配字符串的开头和结尾,而不是行的开头和结尾

    否则:

    """ ab """abc""" abc """

    会匹配

    我也以此作为参考来了解如何排除""":Regular expression to match a line that doesn't contain a word?

    【讨论】:

    • 只是为了澄清,我添加了一个多行标志产生问题的示例。你不应该使用它
    • @jwvh 任何带有美元符号的文本都将停止匹配。
    • @Andreas;你是对的。我的错。忘了[ ]里面的意义调整。
    猜你喜欢
    • 2020-05-05
    • 1970-01-01
    • 2013-06-12
    • 1970-01-01
    • 1970-01-01
    • 2011-03-11
    • 2021-10-29
    • 1970-01-01
    • 2016-08-12
    相关资源
    最近更新 更多