12个月前 (12-11)  Java系列 |   抢沙发  295 
文章评分 1 次,平均分 5.0

当我们需要在Java中查找或替换字符串中的值时,我们通常使用正则表达式。这使我们能够确定字符串的部分或全部是否与模式匹配。使用Matcher和string中的replaceAll方法,我们可以很容易地将相同的替换应用于字符串中的多个标记。

在本文中,我们将探讨如何为字符串中的每个token标记应用不同的替换。

我们还将研究一些调整正则表达式以正确识别标记的技巧。

在我们能够构建标记替换算法之前,我们需要了解围绕正则表达式的Java API。让我们使用捕获组和非捕获组来解决一个棘手的匹配问题。

让我们想象一下,我们想要构建一个算法来处理字符串中的所有标题词。这些单词以一个大写字符开头,然后以小写字符结束或继续。

我们的输入可能是:

"First 3 Capital Words! then 10 TLAs, I Found"

从标题词的定义来看,它包含以下匹配项:

  • First
  • Capital
  • Words
  • I
  • Found

识别这种模式的正则表达式是:

"(?<=^|[^A-Za-z])([A-Z][a-z]*)(?=[^A-Za-z]|$)"

为了理解这一点,让我们将其分解为其组成部分。我们将从中间开始:

[A-Z]

将识别单个大写字母。

我们允许使用单个字符的单词或后面跟着小写字母的单词,因此:

[a-z]*

识别零个或多个小写字母。

在某些情况下,以上两个字符类足以识别我们的令牌。不幸的是,在我们的示例中,有一个单词以多个大写字母开头。因此,我们需要表达的是,我们发现的单个大写字母必须是第一个出现在非字母之后的字母。

同样,当我们允许单个大写字母单词时,我们需要表示我们找到的单个大写字母不能是多个大写字母单词中的第一个。

表达式[^A-Za-z]的意思是“没有字母”。我们将其中一个放在非捕获组中表达式的开头:

(?<=^|[^A-Za-z])

(?<=开头的非捕获组会向后看,以确保匹配项出现在正确的边界上。其末尾的对应组会对后面的字符执行相同的操作。

但是,如果单词接触到了字符串的开头或结尾,那么我们需要考虑到这一点,这就是我们在第一组中添加^|的地方,使其表示“字符串或任何非字母字符的开头”,并且我们在最后一个非捕获组的结尾添加了|$,使字符串的结尾成为边界。

当我们使用find时,在非捕获组中找到的字符不会出现在匹配中。

我们应该注意,即使是像这样的简单用例也可能有许多边缘用例,因此测试我们的正则表达式是很重要的。为此,我们可以编写单元测试,使用IDE的内置工具,或者使用Regexr等在线工具。

使用名为EXAMPLE_INPUT的常量中的示例文本和名为TITLE_CASE_PATTERN的模式中的正则表达式,让我们在Matcher类中使用find来提取单元测试中的所有匹配项:

Matcher matcher = TITLE_CASE_PATTERN.matcher(EXAMPLE_INPUT);
List<String> matches = new ArrayList<>();
while (matcher.find()) {
    matches.add(matcher.group(1));
}

assertThat(matches)
  .containsExactly("First", "Capital", "Words", "I", "Found");

这里我们使用Pattern上的matcher函数来生成matcher。然后,我们在循环中使用find方法,直到它停止返回true以迭代所有匹配项。

每次find返回true时,Matcher对象的状态都设置为表示当前匹配。我们可以使用group(0)检查整个匹配,也可以使用基于1的索引检查特定的捕获group。在本例中,我们需要的工件周围有一个捕获group,因此我们使用group(1)将匹配项添加到列表中。

到目前为止,我们已经找到了想要处理的单词。

但是,如果这些单词中的每一个都是我们想要替换的标记,那么我们需要有关于匹配的更多信息来构建结果字符串。让我们看看Matcher的其他一些属性,它们可能会对我们有所帮助:

while (matcher.find()) {
    System.out.println("Match: " + matcher.group(0));
    System.out.println("Start: " + matcher.start());
    System.out.println("End: " + matcher.end());
}

此代码将显示每个匹配的位置。它还显示group(0)匹配,即捕获的所有内容:

Match: First
Start: 0
End: 5
Match: Capital
Start: 8
End: 15
Match: Words
Start: 16
End: 21
Match: I
Start: 37
End: 38
... more

在这里,我们可以看到每个匹配只包含我们期望的单词。start属性显示字符串中匹配项的从零开始的索引。结尾显示后面字符的索引。这意味着我们可以使用substring(start,end-start)从原始字符串中提取每个匹配项。这基本上就是group方法为我们所做的。

现在我们可以使用find来迭代匹配,让我们来处理标记。

让我们继续我们的例子,使用我们的算法将原始字符串中的每个标题词替换为其对应的小写字母。这意味着我们的测试字符串将转换为:

"first 3 capital words! then 10 TLAs, i found"

PatternMatcher类不能这样做,所以我们需要构造一个算法。

替换算法

以下是算法的伪代码:

  • 以空输出字符串开始
  • 对于每个匹配:
  • 将匹配之前和之前任何匹配之后的任何内容添加到输出中
  • 处理此匹配并将其添加到输出
  • 继续,直到处理完所有匹配项
  • 将上次匹配后剩余的任何内容添加到输出

我们应该注意,该算法的目的是找到所有不匹配的区域并将它们添加到输出中,以及添加处理过的匹配。

我们希望将每个单词转换为小写,因此我们可以编写一个简单的转换方法:

private static String convert(String token) {
    return token.toLowerCase();
}

现在我们可以编写迭代匹配的算法。这可以使用StringBuilder进行输出:

int lastIndex = 0;
StringBuilder output = new StringBuilder();
Matcher matcher = TITLE_CASE_PATTERN.matcher(original);
while (matcher.find()) {
    output.append(original, lastIndex, matcher.start())
      .append(convert(matcher.group(1)));

    lastIndex = matcher.end();
}
if (lastIndex < original.length()) {
    output.append(original, lastIndex, original.length());
}
return output.toString();

我们应该注意,StringBuilder提供了一个方便的append版本,可以提取子字符串。这与Matcherend属性配合得很好,可以让我们提取自上次匹配以来的所有未匹配字符。

既然我们已经解决了替换某些特定标记的问题,为什么不将代码转换成一种可以用于一般情况的形式呢?不同实现之间唯一不同的是要使用的正则表达式,以及将每个匹配转换为替换的逻辑。

我们可以使用Java Function<Matcher,String>对象来允许调用方提供处理每个匹配的逻辑。我们可以使用一个名为tokenPattern的输入来查找所有的标记:

// same as before
while (matcher.find()) {
    output.append(original, lastIndex, matcher.start())
      .append(converter.apply(matcher));

// same as before

这里,正则表达式不再是硬编码的。相反,converter函数由调用者提供,并应用于find循环中的每个匹配。

让我们看看通用方法是否与原始方法一样有效:

assertThat(replaceTokens("First 3 Capital Words! then 10 TLAs, I Found",
  TITLE_CASE_PATTERN,
  match -> match.group(1).toLowerCase()))
  .isEqualTo("first 3 capital words! then 10 TLAs, i found");

在这里,我们看到调用代码非常简单。转换函数易于用lambda表示。测试通过。

现在我们有了一个标记替换器,所以让我们尝试一些其他用例。

特殊字符转义

假设我们想使用正则表达式转义字符\来手动引用正则表达式的每个字符,而不是使用quote方法。也许我们正在引用一个字符串作为创建一个正则表达式以传递给另一个库或服务的一部分,所以块引用表达式是不够的。

如果我们可以表示表示“正则表达式字符”的模式,那么使用我们的算法很容易将它们全部转义:

Pattern regexCharacters = Pattern.compile("[<(\\[{\\\\^\\-=$!|\\]})?*+.>]");

assertThat(replaceTokens("A regex character like [",
  regexCharacters,
  match -> "\\" + match.group()))
  .isEqualTo("A regex character like \\[");

对于每个匹配项,我们都将\字符作为前缀。由于\是Java字符串中的一个特殊字符,因此它用另一个\转义。

实际上,这个例子包含了额外的字符,因为regexCharacters模式中的字符类必须引用许多特殊字符。这显示了正则表达式解析器,我们使用它们来表示它们的文本,而不是正则表达式语法。

替换占位符

表示占位符的常用方法是使用类似${name}的语法。让我们考虑一个用例,其中模板“Hi${name}at${company}”需要从名为placeholderValues的映射中填充:

Map<String, String> placeholderValues = new HashMap<>();
placeholderValues.put("name", "Bill");
placeholderValues.put("company", "Baeldung");

我们只需要一个好的正则表达式来查找${…}标记:

"\\$\\{(?<placeholder>[A-Za-z0-9-_]+)}"

是一种选择。它必须引用$和初始大括号,否则它们将被视为正则表达式语法。

此模式的核心是占位符名称的捕获组。我们使用了一个允许字母数字、破折号和下划线的字符类,这应该适合大多数用例。

但是,为了使代码更具可读性,我们将此捕获组命名为占位符。让我们看看如何使用命名的捕获group:

assertThat(replaceTokens("Hi ${name} at ${company}",
  "\\$\\{(?<placeholder>[A-Za-z0-9-_]+)}",
  match -> placeholderValues.get(match.group("placeholder"))))
  .isEqualTo("Hi Bill at Baeldung");

在这里,我们可以看到从匹配器中获取命名group的值只需要使用名称为输入的group,而不是数字。

结论

在本文中,我们研究了如何使用强大的正则表达式在字符串中查找标记。我们学习了find方法如何与Matcher一起工作,以显示匹配项。

然后,我们创建并推广了一个算法,允许我们逐个标记进行替换。

最后,我们看了几个转义字符和填充模板的常见用例。

代码示例github地址:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-regex-2

原文地址:https://www.baeldung.com/java-regex-token-replacement

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/2937.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册