正则表达式实用指南(二):转移字符、量词和捕获小能手

本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。

你好,我是看山。

正则表达式是Java变成中一把利器,常出现在文本检查、替换等逻辑中。本文中,我们将深入探讨 Java 正则表达式 API,并研究如何在 Java 编程语言中运用正则表达式。

在正则表达式的领域中,存在多种不同的“风格”可供选择,例如 grep、Perl、Python、PHP、awk 等等。这就意味着,在一种编程语言中有效的正则表达式,在另一种语言中可能无法正常工作。Java 中的正则表达式语法与 Perl 中的最为相似。

五、预定义字符类

Java 正则表达式 API 也接受预定义字符类。Java 版本正则表达式的一个特殊之处在于转义字符。

如我们所见,大多数预定义字符类以反斜杠“\”开头,而反斜杠在 Java 中有特殊含义。为了让Pattern类能够正确编译这些字符类,必须对开头的反斜杠进行转义,即\d要写成\\d

匹配数字,等同于[0-9]

int matches = runTest("\\d", "123");

assertEquals(matches, 3);

匹配非数字,等同于[^0-9]

int matches = runTest("\\D", "a6c");

assertEquals(matches, 2);

匹配空白字符:

int matches = runTest("\\s", "a c");

assertEquals(matches, 1);

匹配非空白字符:

int matches = runTest("\\S", "a c");

assertEquals(matches, 2);

匹配单词字符,等同于“[a-zA-Z_0-9]”:

int matches = runTest("\\w", "hi!");

assertEquals(matches, 2);

匹配非单词字符:

int matches = runTest("\\W", "hi!");

assertEquals(matches, 1);

六、量词

Java 正则表达式 API 还允许我们使用量词。通过量词,我们可以指定匹配的次数,进一步调整匹配行为。

要匹配文本零次或一次,我们使用?量词:

int matches = runTest("\\a?", "hi");

assertEquals(matches, 3);

或者,我们也可以使用花括号语法,Java 正则表达式 API 也支持这种方式:

int matches = runTest("\\a{0,1}", "hi");

assertEquals(matches, 3);

这个例子引入了零长度匹配的概念。如果量词的匹配阈值为零,它将匹配文本中的所有内容,包括每个输入末尾的空字符串。这意味着,即使输入为空,它也会返回一个零长度匹配。

这就解释了为什么在上面的例子中,尽管输入字符串的长度为 2,但我们得到了三个匹配。第三个匹配就是零长度的空字符串。

要匹配文本零次或无数次,我们使用*量词,它与``?`类似:

int matches = runTest("\\a*", "hi");

assertEquals(matches, 3);

支持的替代方式:

int matches = runTest("\\a{0,}", "hi");

assertEquals(matches, 3);

*?不同的量词是+,它的匹配阈值为 1。如果所需字符串根本不出现,就不会有匹配,甚至连零长度字符串也不会匹配:

int matches = runTest("\\a+", "hi");

assertEquals(matches, 0);

支持的替代方式:

int matches = runTest("\\a{1,}", "hi");

assertEquals(matches, 0);

与 Perl 和其他语言一样,我们可以使用花括号语法来指定文本的匹配次数:

int matches = runTest("a{3}", "aaaaaa");

assertEquals(matches, 2);

在上述例子中,我们得到两个匹配,因为只有当“a”连续出现三次时才会有匹配。然而,在接下来的测试中,由于文本中“a”只连续出现两次,所以不会有匹配:

int matches = runTest("a{3}", "aa");

assertFalse(matches > 0);

当我们在花括号中使用范围时,匹配将是贪婪的,会从范围的较大端开始匹配:

int matches = runTest("a{2,3}", "aaaa");

assertEquals(matches, 1);

这里我们指定至少出现两次,但不超过三次,所以匹配器会将“aaaa”视为一个“aaa”和一个单独的“a”,只得到一个匹配。

然而,API 允许我们指定一种懒惰或非贪婪的方式,使得匹配器可以从范围的较小端开始匹配,将“aaaa”匹配为“aa”和“aa”:

int matches = runTest("a{2,3}?", "aaaa");

assertEquals(matches, 2);

七、捕获组

API 还允许我们通过捕获组将多个字符视为一个单元。它会为捕获组分配编号,并允许使用这些编号进行反向引用。

在本节中,我们将通过几个示例来了解如何在 Java 正则表达式 API 中使用捕获组。

让我们使用一个捕获组,使其仅在输入文本包含两个相邻数字时进行匹配:

int matches = runTest("(\\d\\d)", "12");

assertEquals(matches, 1);

上述匹配的编号为 1,通过反向引用,我们可以告诉匹配器我们希望匹配文本中已匹配部分的另一次出现。这样,对于输入“1212”,我们可以将其视为一个匹配,而不是两个单独的匹配:

int matches = runTest("(\\d\\d)", "1212");

assertEquals(matches, 2);

我们可以有一个匹配,并通过反向引用使相同的正则表达式匹配扩展到整个输入长度:

int matches = runTest("(\\d\\d)\\1", "1212");

assertEquals(matches, 1);

如果不使用反向引用,我们需要重复正则表达式来实现相同的结果:

int matches = runTest("(\\d\\d)(\\d\\d)", "1212");

assertEquals(matches, 1);

同样,对于任何其他重复次数,反向引用都可以使匹配器将输入视为一个匹配:

int matches = runTest("(\\d\\d)\\1\\1\\1", "12121212");

assertEquals(matches, 1);

但是,如果我们更改最后一个数字,匹配将失败:

int matches = runTest("(\\d\\d)\\1", "1213");

assertFalse(matches > 0);

在 Java 语法中,不要忘记转义反斜杠,这一点至关重要。

文末总结

正则表达式是一个开发利器,用的好的话,会大大提升我们的开发效率。本文介绍了转移字符、量词和捕获组,后续慢慢展开,把正则表达式的各种功能展示出来。

青山不改,绿水长流,我们下次见。

推荐阅读


你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。

👇🏻欢迎关注我的公众号「看山的小屋」,领取精选资料👇🏻

公众号:看山的小屋