1. 概述

在Java编程中,处理字符串和模式对于许多应用至关重要。正则表达式(通常称为regex)提供了一个强大的工具,用于模式匹配和操作。

有时,我们不仅需要在字符串中找到匹配项,还需要确切地定位这些匹配项的位置。在这个教程中,我们将探讨如何在Java中获取正则表达式模式匹配的索引。

2. 问题介绍

让我们以一个字符串为例:

String INPUT = "This line contains <the first value>, <the second value>, and <the third value>.";

假设我们要从上述字符串中提取所有<...>部分,如<the first value><the second value>

为了匹配这些段落,我们可以使用正则表达式的NOR字符类: "<\\[^>\\]\\*>"

在Java中,Regex API中的PatternMatcher类是进行模式匹配的重要工具。这些类提供了编译正则表达式模式并将其应用于字符串进行各种操作的方法。

接下来,我们将使用PatternMatcher来提取所需文本。为了简化,我们将使用AssertJ断言来验证我们是否获得了预期的结果:

Pattern pattern = Pattern.compile("<[^>]*>");
Matcher matcher = pattern.matcher(INPUT);
List<String> result = new ArrayList<>();
while (matcher.find()) {
    result.add(matcher.group());
}
assertThat(result).containsExactly("<the first value>", "<the second value>", "<the third value>");

如上代码所示,我们从输入字符串中提取了所有<...>部分。然而,有时我们希望知道匹配项在输入字符串中的确切位置。换句话说,我们想要获取匹配项及其在输入字符串中的索引。

接下来,我们将扩展这段代码以实现我们的目标。

3. 获取匹配项的索引

我们已经使用Matcher类来提取匹配项。Matcher类提供了两个方法,start()end(),它们允许我们获取每个匹配项的起始和结束索引。值得注意的是,Matcher.end()方法返回匹配子序列最后一个字符之后的索引。

下面的例子可以清楚地说明这一点:

Pattern pattern = Pattern.compile("456");
Matcher matcher = pattern.matcher("0123456789");
String result = null;
int startIdx = -1;
int endIdx = -1;
if (matcher.find()) {
    result = matcher.group();
    startIdx = matcher.start();
    endIdx = matcher.end();
}
assertThat(result).isEqualTo("456");
assertThat(startIdx).isEqualTo(4);
assertThat(endIdx).isEqualTo(7); // matcher.end() returns 7 instead of 6

现在我们了解了start()end()返回的内容,让我们看看是否可以在我们的INPUT:中获取每个"<...>"子序列的索引:

Pattern pattern = Pattern.compile("<[^>]*>");
Matcher matcher = pattern.matcher(INPUT);
List<String> result = new ArrayList<>();
Map<Integer, Integer> indexesOfMatches = new LinkedHashMap<>();
while (matcher.find()) {
    result.add(matcher.group());
    indexesOfMatches.put(matcher.start(), matcher.end());
}
assertThat(result).containsExactly("<the first value>", "<the second value>", "<the third value>");
assertThat(indexesOfMatches.entrySet()).map(entry -> INPUT.substring(entry.getKey(), entry.getValue()))
  .containsExactly("<the first value>", "<the second value>", "<the third value>");

如测试所示,我们将每个匹配项的start()end()结果存储在一个LinkedHashMap中,以保持插入顺序。然后,我们根据这些索引对原始输入进行子串提取。如果我们获得了正确的索引,这些子串必须等于匹配项。

如果运行这个测试,它会通过。

4. 使用捕获组获取匹配项的索引

在正则表达式中,捕获组起着关键作用,它们允许我们在稍后引用它们或方便地提取子模式。

举例来说,假设我们想要提取由<**>包围的内容。在这种情况下,我们可以创建一个包含捕获组的模式:"<(\\[^>\\]\\*)>"。这样,当我们调用Matcher.group(1)时,我们将得到"the first value""the second value"等内容。

如果没有明确定义捕获组,整个正则表达式默认组的索引为0。因此,调用Matcher.group()与调用Matcher.group(0)是相同的。

就像Matcher.group()函数的行为一样,Matcher.start()Matcher.end()方法也支持指定组索引作为参数。因此,这些方法提供了对应于相应组内匹配内容的开始和结束索引:

Pattern pattern = Pattern.compile("<([^>]*)>");
Matcher matcher = pattern.matcher(INPUT);
List<String> result = new ArrayList<>();
Map<Integer, Integer> indexesOfMatches = new LinkedHashMap<>();
while (matcher.find()) {
    result.add(matcher.group(1));
    indexesOfMatches.put(matcher.start(1), matcher.end(1));
}
assertThat(result).containsExactly("the first value", "the second value", "the third value");
assertThat(indexesOfMatches.entrySet()).map(entry -> INPUT.substring(entry.getKey(), entry.getValue()))
  .containsExactly("the first value", "the second value", "the third value");

5. 总结

在这篇文章中,我们探讨了在处理正则表达式时如何在原始输入中获取模式匹配的索引。我们讨论了涉及具有和不具有明确定义的捕获组的模式的情况。

如往常一样,示例代码的完整源码可在GitHub上找到。