带有`\R`的Java-8 正则表达式否定回溯

Posted

技术标签:

【中文标题】带有`\\R`的Java-8 正则表达式否定回溯【英文标题】:Java-8 regex negative lookbehind with `\R`带有`\R`的Java-8 正则表达式否定回溯 【发布时间】:2017-07-17 09:47:27 【问题描述】:

answering another question 时,我编写了一个正则表达式来匹配所有空格,最多包含一个换行符。我使用 \R 换行符匹配器的负面后视来做到这一点:

((?<!\R)\s)*

后来我想了想,我说,哦不,如果有\r\n呢?它肯定会抓住第一个换行符\r,然后我会在下一个字符串的前面被一个虚假的\n 卡住,对吧?

所以我回去测试(并可能修复)它。但是,当我测试该模式时,它匹配整个\r\n。它不只匹配\r 离开\n,正如人们所期望的那样。

"\r\n".matches("((?<!\\R)\\s)*"); // true, expected false

但是,当我将documentation 中提到的“等效”模式用于\R 时,它返回false。那么这是 Java 的一个错误,还是有正当的理由可以匹配?

【问题讨论】:

【参考方案1】:

构造\R 是一个,它将子表达式包围成一个原子组(?&gt; parts )

这就是为什么它不会把它们分开。

注意:如果 Java 在后视中接受固定交替,则使用 \R 是可以的,但如果引擎不接受,则会引发异常。

【讨论】:

@PatrickParker - 我认为它实际上更像(?&gt;\r\n|\n|\r|[stuff])。但是,是的,它被替换为正则表达式字符串,然后重新解析。换句话说,它解析\R,进行替换,然后解析替换。这意味着它被硬编码到引擎中。 @PatrickParker - 原子组 是原子的。这意味着它的子表达式不能被分解,即。回溯到。一旦找到匹配项,组就完成了,它返回一个真/假条件。由于\r\n 始终是交替中的第一个,它总是会匹配(如果找到它)然后返回true。 Assertions 是一样的。从本质上讲,它永远不会超过\r\n 中的\r 看起来 Java 不支持 Casimir 帖子中的 \R。但我将把我的帖子作为\R 构造的历史记录留下。如果它确实支持它,您应该匹配第一个\r,因为它之前没有任何内容,并且在\n 上失败,因为在\R 匹配之前有\r。不是这样的,有些事情发生了。这很可能是一个错误。 @sln: 不是我的错,\R 自 Java8 起可用 我记得 SO 上的一篇文章是关于解析器中的一个错误,该错误在某些情况下无法看到后视中的子模式是可变长度(且不受限制)。也许这是另一个错误。【参考方案2】:

实现#1。文档有误

来源:https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html

这里说:

换行符匹配器

...等价于\u000D\u000A|[\u000A\u000B\u000C\u000D\u0085\u2028\u2029]

但是,当我们尝试使用“等价”模式时,它会返回 false:

String _R_ = "\\R";
System.out.println("\r\n".matches("((?<!"+_R_+")\\s)*")); // true

// using "equivalent" pattern
_R_ = "\\u000D\\u000A|[\\u000A\\u000B\\u000C\\u000D\\u0085\\u2028\\u2029]";
System.out.println("\r\n".matches("((?<!"+_R_+")\\s)*")); // false

// now make it atomic, as per sln's answer
_R_ = "(?>"+_R_+")";
System.out.println("\r\n".matches("((?<!"+_R_+")\\s)*")); // true

所以Javadoc应该真的说:

...等价于(?&lt;!\u000D\u000A|[\u000A\u000B\u000C\u000D\u0085\u2028\u2029])

Oracle 的 Sherman JDK-8176029 于 2017 年 3 月 9 日更新:

"api doc 没有错,实现错了(当"0x0d+0x0a + next.match()" 失败时回溯"0x0d+next.match()" 失败)"


实现#2。 Lookbehinds 不只是向后看

尽管有这个名字,lookbehind 不仅可以向后看,还可以包括甚至跳过当前位置。

考虑以下示例(来自rexegg.com):

"_12_".replaceAll("(?<=_(?=\\d2_))\\d+", "##"); // _##_

“这很有趣,有几个原因。首先,我们在后视中进行前瞻,即使我们应该向后看,这种前瞻通过匹配两个数字和尾随下划线来跳过当前位置。这是杂技。”

对于我们的 \R 示例来说,这意味着即使我们当前的位置可能是 \n,这也不会阻止后向识别它的 \r 后面跟着 \n,然后将两者绑定一起作为一个原子组,因此拒绝将当前位置后面的\r 部分识别为单独的匹配项。

注意:为简单起见,我使用了诸如“我们当前位置是\n”之类的术语,但这并不是内部发生的确切表示。

【讨论】:

仅供参考;关于文档问题,Java Bug Report JDK-8176029 已提交。 只是一个注释。只有在指针 的物理世界中,断言才能存在于某个位置。形而上学,如果这是真的(?&lt;=X)(?=Y),你不能说断言存在于某个位置。因为它可能是真的,所以断言存在于个位置之间,永远不会在一个位置上。 另外,为了意识,断言本质上原子,因为它不能从外部回溯到它的边界。根据定义,它是原子的。不同之处在于,从表面上看,原子组消耗(默认情况下,但取决于它的 internal 构造),而断言不消耗。通过消耗,我的意思是推进当前的目标位置。将 断言 视为存在于 字符位置之间而不是在某个位置的构造,这一点始终很重要。 @sln - 你的笔记有点混乱。如果断言本质上是原子的,那么您如何解释我的示例代码中的两个不同结果?另外,请查看 JDK 错误报告。他们确实认为这种行为毕竟是一个错误。 @PatrickParker:文档错了,“修复”错了,见***.com/a/47879236/371250

以上是关于带有`\R`的Java-8 正则表达式否定回溯的主要内容,如果未能解决你的问题,请参考以下文章

如何在 java 8 正则表达式中使用 \R [重复]

Go 正则表达式中没有灾难性的回溯吗?

使用正则表达式解析 C 样式注释,避免回溯

JS 正则表达式否定匹配(正向前瞻)

正则表达式的否定

正则表达式否定后缀否定环视不起作用