为什么 find() with Regex ^[a-z]$ 不等同于 matches() with Regex [a-z]?
Why is find() with Regex ^[a-z]$ not equivalent to matches() with Regex [a-z]?
Java 的 Matcher
is the engine that performs match operations on a character sequence by interpreting a Pattern
(正则表达式)。这个 class 有两个众所周知的操作:
Matcher.find()
扫描输入序列以查找下一个与模式匹配的子序列。
Matcher.matches()
尝试将整个输入序列与模式匹配。
换句话说,find()
应用于匹配子字符串,而 matches()
应用于匹配整个输入。这让我想到将 find()
与 ^[a-z]$
这样的正则表达式一起使用等同于将 matches()
与 [a-z]
这样的正则表达式一起使用,所以我继续进行测试。
Click here to run below code online.
import java.util.List;
import java.util.regex.Pattern;
public class Main
{
public static void main(String[] args) {
Pattern sub = Pattern.compile("[a-z]+");
Pattern all = Pattern.compile("^[a-z]+$");
List<String> tests = List.of("", " ", "a", "A", "abc", "a\r",
"a\r\n", "a\n", " a", "\na", "\ra\n",
"\r\na", "\na");
for (String test : tests) {
boolean matchesSub = sub.matcher(test).matches();
boolean matchesAll = all.matcher(test).find();
System.out.printf("%s\t%s\t%s", format(test), matchesSub, matchesAll);
System.out.println();
}
}
private static String format(String input) {
return input.replace("\r", "\r").replace("\n", "\n");
}
}
产生了以下输出:
false false
false false
a true true
A false false
abc true true
a\r false true
a\r\n false true
a\n false true
a false false
\na false false
\ra\n false false
\r\na false false
\na false false
有趣的是,此测试失败 a\r
、a\r\n
和 a\n
:
- 在这些情况下使用
matches()
和 [a-z]+
会产生 false
。显然最后的换行符被算作一个字符,未通过测试。
- 在这些情况下使用
find()
和 ^[a-z]+$
会产生 true
。显然最后的换行符被忽略了,通过了测试。
这仅适用于换行符位于末尾而不是开头的情况,因为两种方法对 \r\na
的处理相同。
怎么回事?
正如@WiktorStribiżew 在他的评论中回答的那样,matches()
和 [a-z]+
是 NOT 等同于 find()
和 ^[a-z]+$
,然而它 是 等同于 find()
和 ^[a-z]+\z
。这是因为 $
将单个尾随换行符视为一种特殊情况:它会忽略它。 \z
没那么宽容。
官方 Java 文档中没有清楚地记录此行为。此外,还有an open bug report in the JDK currently under investigation which specifically deals with the $
matcher, trailing newlines and the find()
method. Also, judging by these other older reports it's at the minimum confusing: JDK-8218146 JDK-8059325 JDK-8058923 JDK-8049849 JDK-8043255
最后,这个行为是not the same in all RegEx implementations:
In all major engines except JavaScript, if the string has one final line break, the $ anchor can match there. For instance, in the apple\n, e$ matches the final e.
^
和 $
表示不同的东西,具体取决于您 运行 您的正则表达式所处的模式。请参阅 Pattern.MULTILINE
标志的 javadoc。
无论如何,^
和$
从不消耗任何东西。
正则表达式引擎的工作方式是,正则表达式中的所有内容都可以 'match' 或 'not match' 和 通常 作为匹配的一部分,它们也消耗字符.
你可以把它想象成一个光标,就像你的文本光标总是在字符之间一样,正则表达式引擎将从左到右通过你的正则表达式,在输入的开头开始光标,并且对于正则表达式模式中的每个项目,该项目匹配或失败,并且通常但不总是向前移动光标。
^
和$
可以匹配或失败,但不能移动光标。它与例如相同\b
(在 'word break' 上匹配),或 (positive/negative) 以这种方式查找 (ahead/behind)。这里的相关技巧是,对于 matches()
的情况,必须消耗每个字符 - 匹配过程 必须 结束,这样光标就在最后。您的模式只能使用小写字母(只有在有小写字母时才向前移动光标),所以当您在字符串中抛出任何不是其中之一的字符时(所以即使是一个 \r
或 \n
, 在任何位置), 它不可能匹配;无法使用这些非小写字符。
另一方面,使用find()
,您不需要消耗所有字符;你只需要一个子字符串来匹配,仅此而已。
然后我们得到:字符串中的哪些 'states' 被认为是 'matching' ^
状态,哪些被认为是 'matching' $
状态。答案部分取决于 MULTILINE
模式是否开启。它在您的代码片段中已关闭;您可以通过使用 Pattern.compile(patternString, Pattern.MULTILINE)
制作您的正则表达式或通过在您的正则表达式字符串中扔 (?m)
来打开它((?xyz)
enables/disables 从您的模式字符串中显示的点开始的标志,并且没有其他效果(总是匹配,不消耗任何东西 - 这是 regexp-engine-ese 的:不做任何事情)。
甚至 UNIX_LINES
对此也有影响(打开 UNIX_LINES
模式,只有 \n
被认为是行终止,并且 ^
/$
如果您处于 MULTILINE 模式,只要您在线路终端上就会匹配。
在多行模式下,您的所有示例都非常匹配; ^
是 'true' 任何时候光标在输入开始处(光标总是在字符之间;如果它在开始和第一个字符之间(即在第一个字符之前),它被认为是匹配的)——或者如果你在换行符和紧跟在它后面的东西之间,只要那个东西不是整个输入的结尾。 \r
和 \n
都算在内(因为 UNIX_LINES
已关闭)。
但是你没有在多线模式下,所以在火焰中发生了什么?
这是怎么回事,文档有误。正如@MartinDevillers 出色地挖掘相关错误条目所示。
文档只是略有错误。具体来说,正则表达式引擎正试图变得比死记硬背更智能:
来自正则表达式包的javadoc:
By default these expressions only match at the beginning and the end of the entire input sequence.
这简直就是废话。它比这更智能:当您的光标位于一个字符和一个换行符之间时,它们 也 匹配,尽管 \r
、\n
和 \r\n
都被认为是 'one newline',只要一个换行符是整个输入中的最后一件事。换句话说,给定(每个 space 都不是真实的;我正在腾出空间来显示光标可以在哪里,它只能在字符之间,所以我可以在它们下面贴一个标记来显示匹配的地方):
" h e l l o \r \n "
^ ^ ^
匹配系统认为 $
在任何 ^
个地方匹配。让我们来验证一下这个理论:
Pattern p = Pattern.compile("hello$");
System.out.println(p.matcher("hello\r\n\n").find());
System.out.println(p.matcher("hello\r\n").find());
System.out.println(p.matcher("hello\r").find());
System.out.println(p.matcher("hello\n").find());
System.out.println(p.matcher("hello\n\n").find());
这会打印 false、true、true、true、false。中间的 3 个在末尾都有一个字符(或多个字符)被认为是 'a single newline' 在至少一个主要 OS 上(\n
是 posix/unix/macosx,\r\n
是 windows,\r
是经典的 mac,我不认为 运行 是 JVM,也没有人再使用了,但它仍然被大多数人认为是 'a newline' g运行dfathering 原因我猜的规则)。
这就是你所缺少的。
结论:
文档略有错误,$
比 'matches at very end of input' 更聪明;它承认有时输入的末尾有一个杂散的换行符,$
不会因此而感到困惑。但是 matches()
会在最后被一个悬空的换行符搞糊涂——它必须消耗所有东西或者它不被认为是匹配的。
Java 的 Matcher
is the engine that performs match operations on a character sequence by interpreting a Pattern
(正则表达式)。这个 class 有两个众所周知的操作:
Matcher.find()
扫描输入序列以查找下一个与模式匹配的子序列。Matcher.matches()
尝试将整个输入序列与模式匹配。
换句话说,find()
应用于匹配子字符串,而 matches()
应用于匹配整个输入。这让我想到将 find()
与 ^[a-z]$
这样的正则表达式一起使用等同于将 matches()
与 [a-z]
这样的正则表达式一起使用,所以我继续进行测试。
Click here to run below code online.
import java.util.List;
import java.util.regex.Pattern;
public class Main
{
public static void main(String[] args) {
Pattern sub = Pattern.compile("[a-z]+");
Pattern all = Pattern.compile("^[a-z]+$");
List<String> tests = List.of("", " ", "a", "A", "abc", "a\r",
"a\r\n", "a\n", " a", "\na", "\ra\n",
"\r\na", "\na");
for (String test : tests) {
boolean matchesSub = sub.matcher(test).matches();
boolean matchesAll = all.matcher(test).find();
System.out.printf("%s\t%s\t%s", format(test), matchesSub, matchesAll);
System.out.println();
}
}
private static String format(String input) {
return input.replace("\r", "\r").replace("\n", "\n");
}
}
产生了以下输出:
false false
false false
a true true
A false false
abc true true
a\r false true
a\r\n false true
a\n false true
a false false
\na false false
\ra\n false false
\r\na false false
\na false false
有趣的是,此测试失败 a\r
、a\r\n
和 a\n
:
- 在这些情况下使用
matches()
和[a-z]+
会产生false
。显然最后的换行符被算作一个字符,未通过测试。 - 在这些情况下使用
find()
和^[a-z]+$
会产生true
。显然最后的换行符被忽略了,通过了测试。
这仅适用于换行符位于末尾而不是开头的情况,因为两种方法对 \r\na
的处理相同。
怎么回事?
正如@WiktorStribiżew 在他的评论中回答的那样,matches()
和 [a-z]+
是 NOT 等同于 find()
和 ^[a-z]+$
,然而它 是 等同于 find()
和 ^[a-z]+\z
。这是因为 $
将单个尾随换行符视为一种特殊情况:它会忽略它。 \z
没那么宽容。
官方 Java 文档中没有清楚地记录此行为。此外,还有an open bug report in the JDK currently under investigation which specifically deals with the $
matcher, trailing newlines and the find()
method. Also, judging by these other older reports it's at the minimum confusing: JDK-8218146 JDK-8059325 JDK-8058923 JDK-8049849 JDK-8043255
最后,这个行为是not the same in all RegEx implementations:
In all major engines except JavaScript, if the string has one final line break, the $ anchor can match there. For instance, in the apple\n, e$ matches the final e.
^
和 $
表示不同的东西,具体取决于您 运行 您的正则表达式所处的模式。请参阅 Pattern.MULTILINE
标志的 javadoc。
无论如何,^
和$
从不消耗任何东西。
正则表达式引擎的工作方式是,正则表达式中的所有内容都可以 'match' 或 'not match' 和 通常 作为匹配的一部分,它们也消耗字符.
你可以把它想象成一个光标,就像你的文本光标总是在字符之间一样,正则表达式引擎将从左到右通过你的正则表达式,在输入的开头开始光标,并且对于正则表达式模式中的每个项目,该项目匹配或失败,并且通常但不总是向前移动光标。
^
和$
可以匹配或失败,但不能移动光标。它与例如相同\b
(在 'word break' 上匹配),或 (positive/negative) 以这种方式查找 (ahead/behind)。这里的相关技巧是,对于 matches()
的情况,必须消耗每个字符 - 匹配过程 必须 结束,这样光标就在最后。您的模式只能使用小写字母(只有在有小写字母时才向前移动光标),所以当您在字符串中抛出任何不是其中之一的字符时(所以即使是一个 \r
或 \n
, 在任何位置), 它不可能匹配;无法使用这些非小写字符。
另一方面,使用find()
,您不需要消耗所有字符;你只需要一个子字符串来匹配,仅此而已。
然后我们得到:字符串中的哪些 'states' 被认为是 'matching' ^
状态,哪些被认为是 'matching' $
状态。答案部分取决于 MULTILINE
模式是否开启。它在您的代码片段中已关闭;您可以通过使用 Pattern.compile(patternString, Pattern.MULTILINE)
制作您的正则表达式或通过在您的正则表达式字符串中扔 (?m)
来打开它((?xyz)
enables/disables 从您的模式字符串中显示的点开始的标志,并且没有其他效果(总是匹配,不消耗任何东西 - 这是 regexp-engine-ese 的:不做任何事情)。
甚至 UNIX_LINES
对此也有影响(打开 UNIX_LINES
模式,只有 \n
被认为是行终止,并且 ^
/$
如果您处于 MULTILINE 模式,只要您在线路终端上就会匹配。
在多行模式下,您的所有示例都非常匹配; ^
是 'true' 任何时候光标在输入开始处(光标总是在字符之间;如果它在开始和第一个字符之间(即在第一个字符之前),它被认为是匹配的)——或者如果你在换行符和紧跟在它后面的东西之间,只要那个东西不是整个输入的结尾。 \r
和 \n
都算在内(因为 UNIX_LINES
已关闭)。
但是你没有在多线模式下,所以在火焰中发生了什么?
这是怎么回事,文档有误。正如@MartinDevillers 出色地挖掘相关错误条目所示。
文档只是略有错误。具体来说,正则表达式引擎正试图变得比死记硬背更智能:
来自正则表达式包的javadoc:
By default these expressions only match at the beginning and the end of the entire input sequence.
这简直就是废话。它比这更智能:当您的光标位于一个字符和一个换行符之间时,它们 也 匹配,尽管 \r
、\n
和 \r\n
都被认为是 'one newline',只要一个换行符是整个输入中的最后一件事。换句话说,给定(每个 space 都不是真实的;我正在腾出空间来显示光标可以在哪里,它只能在字符之间,所以我可以在它们下面贴一个标记来显示匹配的地方):
" h e l l o \r \n "
^ ^ ^
匹配系统认为 $
在任何 ^
个地方匹配。让我们来验证一下这个理论:
Pattern p = Pattern.compile("hello$");
System.out.println(p.matcher("hello\r\n\n").find());
System.out.println(p.matcher("hello\r\n").find());
System.out.println(p.matcher("hello\r").find());
System.out.println(p.matcher("hello\n").find());
System.out.println(p.matcher("hello\n\n").find());
这会打印 false、true、true、true、false。中间的 3 个在末尾都有一个字符(或多个字符)被认为是 'a single newline' 在至少一个主要 OS 上(\n
是 posix/unix/macosx,\r\n
是 windows,\r
是经典的 mac,我不认为 运行 是 JVM,也没有人再使用了,但它仍然被大多数人认为是 'a newline' g运行dfathering 原因我猜的规则)。
这就是你所缺少的。
结论:
文档略有错误,$
比 'matches at very end of input' 更聪明;它承认有时输入的末尾有一个杂散的换行符,$
不会因此而感到困惑。但是 matches()
会在最后被一个悬空的换行符搞糊涂——它必须消耗所有东西或者它不被认为是匹配的。