1. 概述
在这篇快速教程中,我们将聚焦于 Java 中字符串的子串提取功能。
我们主要使用 String 类的方法,也会涉及 Apache Commons 的 StringUtils 类的一些实用方法。
在以下所有示例中,我们将使用如下字符串:
String text = "Julia Evans was born on 25-09-1984. "
+ "She is currently living in the USA (United States of America).";
2. substring 基础用法
我们先来看一个非常简单的例子:通过起始索引提取子串:
assertEquals("USA (United States of America).",
text.substring(67));
这个例子中,我们提取了 Julia 的居住国家信息。
我们也可以指定结束索引,如果不指定,substring 会一直提取到字符串末尾。
现在我们去掉结尾多余的句号:
assertEquals("USA (United States of America)",
text.substring(67, text.length() - 1));
上面的例子中,我们通过精确位置提取了子串。
2.1. 根据特定字符动态获取子串
如果位置需要根据某个字符或字符串动态计算,我们可以使用 indexOf 方法:
assertEquals("United States of America",
text.substring(text.indexOf('(') + 1, text.indexOf(')')));
还有一个类似的方法是 lastIndexOf。我们用它来提取年份“1984”,即最后一个短横线和第一个句号之间的内容:
assertEquals("1984",
text.substring(text.lastIndexOf('-') + 1, text.indexOf('.')));
indexOf 和 lastIndexOf 都可以接受字符或字符串作为参数。我们来提取“USA”及括号中的内容:
assertEquals("USA (United States of America)",
text.substring(text.indexOf("USA"), text.indexOf(')') + 1));
3. 使用 subSequence
String 类提供了一个与 substring 类似的方法 subSequence。
*唯一的区别是它返回的是 CharSequence 而不是 String,并且必须同时指定起始和结束索引:*
assertEquals("USA (United States of America)",
text.subSequence(67, text.length() - 1));
4. 使用正则表达式
如果我们要提取符合特定模式的子串,正则表达式会派上用场。
在示例字符串中,Julia 的出生日期格式为“dd-mm-yyyy”。我们可以使用 Java 的正则表达式 API 来匹配这个模式。
首先,我们创建一个匹配“dd-mm-yyyy”的模式:
Pattern pattern = Pattern.compile("\\d{2}-\\d{2}-\\d{4}");
然后,我们将该模式应用于文本中查找匹配项:
Matcher matcher = pattern.matcher(text);
匹配成功后,我们可以提取匹配的字符串:
if (matcher.find()) {
Assert.assertEquals("25-09-1984", matcher.group());
}
更多关于 Java 正则表达式的知识,可以参考 这篇教程。
5. 使用 split
我们可以使用 String 类的 split 方法来提取子串。比如,我们要提取示例字符串中的第一句话:
String[] sentences = text.split("\\.");
由于 split 方法接受正则表达式作为参数,我们需要对句号进行转义。结果是一个包含两句话的数组。
我们取第一句话(或遍历整个数组):
assertEquals("Julia Evans was born on 25-09-1984", sentences[0]);
⚠️ 注意:如果需要更准确的句子检测和分词,推荐使用 Apache OpenNLP。可以参考 这篇教程 了解更多。
6. 使用 Scanner
我们通常使用 Scanner 来解析原始类型和字符串,支持使用正则表达式。
Scanner 会根据分隔符模式将输入分割为多个 token,默认分隔符是空白字符。
我们来看看如何使用 Scanner 提取第一句话:
try (Scanner scanner = new Scanner(text)) {
scanner.useDelimiter("\\.");
assertEquals("Julia Evans was born on 25-09-1984", scanner.next());
}
在这个例子中,我们将句号作为分隔符(需要转义),然后断言第一个 token。
如果需要,我们可以通过 while 循环遍历所有 token:
while (scanner.hasNext()) {
// do something with the tokens returned by scanner.next()
}
7. Maven 依赖
我们可以进一步使用一个非常实用的工具类:Apache Commons Lang 库中的 StringUtils:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
你可以在 Maven Central 上找到最新版本。
8. 使用 StringUtils
Apache Commons 库提供了许多用于操作 Java 核心类型的实用方法。Apache Commons Lang 为 java.lang API 提供了大量辅助工具,尤其是字符串操作方法。
在这个例子中,我们将看到 如何提取两个字符串之间的子串:
assertEquals("United States of America",
StringUtils.substringBetween(text, "(", ")"));
如果子串被两个相同的字符串包围,还有一个简化版本的方法:
substringBetween(String str, String tag)
同一个类中的 substringAfter 方法可以获取第一个分隔符之后的子串。
分隔符本身不会被返回:
assertEquals("the USA (United States of America).",
StringUtils.substringAfter(text, "living in "));
类似地,substringBefore 方法获取第一个分隔符之前的子串。
分隔符也不会被返回:
assertEquals("Julia Evans",
StringUtils.substringBefore(text, " was born"));
更多关于 Apache Commons Lang 的字符串处理功能,可以参考相关教程。
9. 提取子串前后的文本
前面我们探讨了多种提取子串的方法。有时候,我们的输入中包含特定子串,但我们不是要提取该子串本身,而是 提取它前后的文本。
举个例子,假设我们要提取以下子串前后的文本:
"was born on 25-09-1984. She "
期望结果为:
Before: "Julia Evans "
After: "is currently living in the USA (United States of America)."
为了简化,我们假设输入字符串中该子串只出现一次。
9.1. 使用 substring() 方法
我们已经知道如何使用 substring() 提取子串。现在我们再次使用它来解决这个问题:
String substring = "was born on 25-09-1984. She ";
int startIdx = text.indexOf(substring);
String before = text.substring(0, startIdx);
String after = text.substring(startIdx + substring.length());
assertEquals("Julia Evans ", before);
assertEquals("is currently living in the USA (United States of America).", after);
如代码所示,使用 indexOf() 和 substring() 的组合可以解决问题。
9.2. 使用 split() 方法
另一种思路是将子串视为分隔符,使用 split() 方法提取前后文本:
String substring = "was born on 25-09-1984. She ";
String[] result = text.split(Pattern.quote(substring));
assertEquals(2, result.length);
String before = result[0];
String after = result[1];
assertEquals("Julia Evans ", before);
assertEquals("is currently living in the USA (United States of America).", after);
可以看到,split() 返回一个包含两个元素的字符串数组:前后文本。
这个方法是有效的。但要注意的是,*我们使用了 Pattern.quote(substring) 而不是直接传入 substring。*
这是因为 split() 接受正则表达式作为参数。Pattern.quote(substring) 告诉正则引擎将 substring 视为字面字符串,即其中的字符不具有特殊含义。
举个例子说明区别:
String input = "This is an *important* issue.";
String substring = " *important* ";
String[] resultWithoutQuote = input.split(substring);
assertEquals(1, resultWithoutQuote.length);
assertEquals(input, resultWithoutQuote[0]);
测试结果表明,如果直接传入 substring,返回的数组只有一个元素:整个输入字符串。这是因为 substring 中的某些字符具有正则表达式的特殊含义。
而使用 Pattern.quote() 后,结果正确:
String[] result = input.split(Pattern.quote(substring));
String before = result[0];
String after = result[1];
assertEquals("This is an", before);
assertEquals("issue.", after);
✅ 所以,当我们希望将正则表达式作为字面字符串处理时,应使用 Pattern.quote() 方法。
10. 提取两个字符串之间的文本
接下来我们来看另一个实际问题,并使用前面学到的技术来解决。
假设有如下输入字符串:
String input = "a <%One%> b <%Two%> c <%Three%>";
可以看到,文本中的一些词被“<%”和“%>”包围。我们的目标是提取这些子串。由于有多个“<% … %>”对,我们希望得到一个列表:
List<String> expected = List.of("One", "Two", "Three");
我们可以使用正则表达式的 Pattern 和 Matcher 来完成这个任务。首先,我们需要构建一个匹配“<%”和“%>”之间文本的正则表达式。这很简单:
"<%(.*)%>"
我们将中间的文本放在捕获组中以便提取。
接下来,我们测试这个正则表达式是否有效:
Pattern pattern = Pattern.compile("<%(.*)%>");
Matcher matcher = pattern.matcher(input);
List<String> result = new ArrayList<>();
while (matcher.find()) {
result.add(matcher.group(1));
}
assertEquals(1, result.size());
assertEquals("One%> b <%Two%> c <%Three", result.get(0));
测试结果表明,我们没有得到预期的三个元素列表,而是一个元素。这是因为“*”是正则表达式中的贪婪量词。 换句话说,它会匹配从第一个“<%”到最后一个“%>”之间的所有内容。
✅ 修复方法很简单:在“*”后添加“?”,使其变为非贪婪匹配:
"<%(.*?)%>"
现在,正则表达式会匹配每一对“<%”和“%>”之间的内容。
再次测试:
Pattern pattern = Pattern.compile("<%(.*?)%>");
Matcher matcher = pattern.matcher(input);
List<String> result = new ArrayList<>();
while (matcher.find()) {
result.add(matcher.group(1));
}
assertEquals(expected, result);
✅ 测试通过,我们成功提取了期望的字符串列表。
11. 总结
在这篇快速教程中,我们介绍了多种在 Java 中提取子串的方法。我们还提供了 其他字符串操作教程 供进一步学习。
代码示例可以在 GitHub 上找到。