带有`\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
是一个宏,它将子表达式包围成一个原子组(?> parts )
。
这就是为什么它不会把它们分开。
注意:如果 Java 在后视中接受固定交替,则使用 \R
是可以的,但如果引擎不接受,则会引发异常。
【讨论】:
@PatrickParker - 我认为它实际上更像(?>\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应该真的说:
...等价于
(?<!\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 已提交。 只是一个注释。只有在指针 的物理世界中,断言才能存在于某个位置。形而上学,如果这是真的(?<=X)(?=Y)
,你不能说断言存在于某个位置。因为它可能是真的,所以断言存在于在个位置之间,永远不会在一个位置上。
另外,为了意识,断言是本质上原子,因为它不能从外部回溯到它的边界。根据定义,它是原子的。不同之处在于,从表面上看,原子组消耗(默认情况下,但取决于它的 internal 构造),而断言不消耗。通过消耗,我的意思是推进当前的目标位置。将 断言 视为存在于 字符位置之间而不是在某个位置的构造,这一点始终很重要。
@sln - 你的笔记有点混乱。如果断言本质上是原子的,那么您如何解释我的示例代码中的两个不同结果?另外,请查看 JDK 错误报告。他们确实认为这种行为毕竟是一个错误。
@PatrickParker:文档错了,“修复”错了,见***.com/a/47879236/371250以上是关于带有`\R`的Java-8 正则表达式否定回溯的主要内容,如果未能解决你的问题,请参考以下文章