我可以。在Java正则表达式中,我可以替换组。

132
我有这段代码,想知道在Java正则表达式中是否可以仅替换组(而不是所有模式)。
代码:
 //...
 Pattern p = Pattern.compile("(\\d).*(\\d)");
    String input = "6 example input 4";
    Matcher m = p.matcher(input);
    if (m.find()) {

        //Now I want replace group one ( (\\d) ) with number 
       //and group two (too (\\d) ) with 1, but I don't know how.

    }

13
你能否澄清一下你的问题,比如给出该输入所期望的输出? - Michael Myers
7个回答

160

replaceFirst(...)中使用$n(其中n是数字)来引用捕获的子序列。我假设您想用字面字符串"number"替换第一组,并用第一组的值替换第二组。

Pattern p = Pattern.compile("(\\d)(.*)(\\d)");
String input = "6 example input 4";
Matcher m = p.matcher(input);
if (m.find()) {
    // replace first number with "number" and second number with the first
    // the added group ("(.*)" which is $2) captures unmodified text to include it in the result
    String output = m.replaceFirst("number$2$1"); // "number example input 6"
}
考虑将第二个组改为(\D+)而不是(.*)* 是贪婪匹配器,会首先消耗掉最后一个数字。匹配器将在意识到最后的(\d)没有任何匹配项之前进行回溯,然后才能匹配到最后一个数字。 编辑 多年以后,这仍然会获得投票,并且评论和编辑(破坏了答案)表明仍然存在关于问题意思的混淆。我已经修复了它,并添加了非常需要的示例输出。
替换的编辑(有些人认为不应该使用$2)实际上破坏了答案。尽管持续的投票显示了答案的关键点-在replaceFirst(...)内使用$n引用来重复使用捕获的值-但编辑失去了未修改的文本需要被捕获并用于替换的事实,以便“只有组(而不是所有模式)”。
问题,因此这个答案,与迭代无关。这是故意制作的MRE

12
如果您能够发布一个示例输出,那就太好了。 - winklerrr
7
如果有许多组并且您正在使用while(m.find())进行迭代,那么这仅适用于第一个匹配项,但不适用于其他匹配项。 - Hugo Zaragoza
2
我同意Hugo的观点,这是一种可怕的实现解决方案...为什么这被接受为答案,而不是acdcjunior的答案——这是完美的解决方案:代码量少,高内聚低耦合,出现意外副作用的可能性大大降低(如果不是没有)...叹气... - FireLight
这个答案目前是无效的。m.replaceFirst("number $2$1"); 应该改为 m.replaceFirst("number $3$1"); - Daniel Eisenreich
这是回答第一组的问题,而不是 OP 的问题“我是否可以仅替换组”,与 @acdcjunior 的回答不同。 - desgraci
1
如果我们想要在这个答案中提供的输出是"number example input 6",为了避免与组号$n混淆,你可以给组起名字,例如start、middle、end: Pattern p = Pattern.compile("(?<start>\\d)(?<middle>.*)(?<end>\\d)"); 然后引用: m.replaceFirst("number${middle}${start}"); - Piotr Boho

74
你可以使用 Matcher#start(group)Matcher#end(group) 来构建一个通用的替换方法:
public static String replaceGroup(String regex, String source, int groupToReplace, String replacement) {
    return replaceGroup(regex, source, groupToReplace, 1, replacement);
}

public static String replaceGroup(String regex, String source, int groupToReplace, int groupOccurrence, String replacement) {
    Matcher m = Pattern.compile(regex).matcher(source);
    for (int i = 0; i < groupOccurrence; i++)
        if (!m.find()) return source; // pattern not met, may also throw an exception here
    return new StringBuilder(source).replace(m.start(groupToReplace), m.end(groupToReplace), replacement).toString();
}

public static void main(String[] args) {
    // replace with "%" what was matched by group 1 
    // input: aaa123ccc
    // output: %123ccc
    System.out.println(replaceGroup("([a-z]+)([0-9]+)([a-z]+)", "aaa123ccc", 1, "%"));

    // replace with "!!!" what was matched the 4th time by the group 2
    // input: a1b2c3d4e5
    // output: a1b2c3d!!!e5
    System.out.println(replaceGroup("([a-z])(\\d)", "a1b2c3d4e5", 2, 4, "!!!"));
}

在此在线演示中查看。


2
这个回答应该是被接受的,因为它是最完整和“可用”的解决方案,而不会引入与相应代码的耦合程度。虽然我建议更改其中一个方法的名称。乍一看,第一个方法看起来像是递归调用。 - FireLight
错过了编辑机会。撤回有关递归调用的部分,没有正确分析代码。这些重载函数很好地配合使用。 - FireLight
这个解决方案只适用于替换单个出现和一个组,因为每次替换都需要复制整个字符串,所以对于其他任何目的来说都不太优秀。但它是一个很好的起点。可惜Java有很多无意义的东西,但缺乏基本的字符串操作功能。 - 9ilsdx 9rvj 0lo

40

很抱歉要老调重谈,但有点奇怪没有人指出来 - “是的,你可以这样做,但这与在实际生活中使用捕获组的方式相反。”

如果您按照正则表达式的本意使用它,则解决方案就像这样简单:

"6 example input 4".replaceAll("(?:\\d)(.*)(?:\\d)", "number$11");

正如shmosel在下面正确指出的那样,

"6 example input 4".replaceAll("\d(.*)\d", "number$11");

由于在您的正则表达式中,没有必要对小数进行分组。

通常情况下,您不会在您想丢弃的字符串部分使用捕获组,而是在您想保留的字符串部分使用它们。

如果您真的需要用于替换的组,那么您可能需要使用模板引擎(例如Moustache、ejs、StringTemplate等)。


顺便说一句,即使在正则表达式中,非捕获组也只是为了让正则表达式引擎识别和跳过可变文本。例如,在...

(?:abc)*(capture me)(?:bcd)*

如果你的输入可能看起来像"abcabc捕获我bcdbcd"或者"abc捕获我bcd"甚至只是"捕获我",那么你需要它们。

或者反过来说:如果文本始终相同,并且您不捕获它,则根本没有使用组的理由。


2
非捕获组是不必要的;\d(.*)\d就足够了。 - shmosel
3
我不理解这里的“$11”。为什么是11? - Alexis
1
@Alexis - 这是一个 Java 正则表达式的小问题:如果第 11 组没有设置,Java 会将 $11 解释为 $1 后面跟着数字 1。 - Yaro
这种方法不会导致正则表达式在每次使用时都被编译吗?是否有一种类似的方法可以使用预编译的“Pattern”? - Garret Wilson

4

将输入中的密码字段替换:

{"_csrf":["9d90c85f-ac73-4b15-ad08-ebaa3fa4a005"],"originPassword":["uaas"],"newPassword":["uaas"],"confirmPassword":["uaas"]}



  private static final Pattern PATTERN = Pattern.compile(".*?password.*?\":\\[\"(.*?)\"\\](,\"|}$)", Pattern.CASE_INSENSITIVE);

  private static String replacePassword(String input, String replacement) {
    Matcher m = PATTERN.matcher(input);
    StringBuffer sb = new StringBuffer();
    while (m.find()) {
      Matcher m2 = PATTERN.matcher(m.group(0));
      if (m2.find()) {
        StringBuilder stringBuilder = new StringBuilder(m2.group(0));
        String result = stringBuilder.replace(m2.start(1), m2.end(1), replacement).toString();
        m.appendReplacement(sb, result);
      }
    }
    m.appendTail(sb);
    return sb.toString();
  }

  @Test
  public void test1() {
    String input = "{\"_csrf\":[\"9d90c85f-ac73-4b15-ad08-ebaa3fa4a005\"],\"originPassword\":[\"123\"],\"newPassword\":[\"456\"],\"confirmPassword\":[\"456\"]}";
    String expected = "{\"_csrf\":[\"9d90c85f-ac73-4b15-ad08-ebaa3fa4a005\"],\"originPassword\":[\"**\"],\"newPassword\":[\"**\"],\"confirmPassword\":[\"**\"]}";
    Assert.assertEquals(expected, replacePassword(input, "**"));
  }

3
这里有一种不同的解决方案,还允许在多个匹配中替换单个组。它使用堆栈来反转执行顺序,因此可以安全地执行字符串操作。
private static void demo () {

    final String sourceString = "hello world!";

    final String regex = "(hello) (world)(!)";
    final Pattern pattern = Pattern.compile(regex);

    String result = replaceTextOfMatchGroup(sourceString, pattern, 2, world -> world.toUpperCase());
    System.out.println(result);  // output: hello WORLD!
}

public static String replaceTextOfMatchGroup(String sourceString, Pattern pattern, int groupToReplace, Function<String,String> replaceStrategy) {
    Stack<Integer> startPositions = new Stack<>();
    Stack<Integer> endPositions = new Stack<>();
    Matcher matcher = pattern.matcher(sourceString);

    while (matcher.find()) {
        startPositions.push(matcher.start(groupToReplace));
        endPositions.push(matcher.end(groupToReplace));
    }
    StringBuilder sb = new StringBuilder(sourceString);
    while (! startPositions.isEmpty()) {
        int start = startPositions.pop();
        int end = endPositions.pop();
        if (start >= 0 && end >= 0) {
            sb.replace(start, end, replaceStrategy.apply(sourceString.substring(start, end)));
        }
    }
    return sb.toString();       
}

3
您可以使用matcher.start()和matcher.end()方法来获取组的位置。因此,使用这些位置,您可以轻松替换任何文本。

0
自 Java 9 开始,您可以使用 Matcher.replaceAll。用法如下:
Pattern p = Pattern.compile("(\\d)(.*)(\\d)");
String input = "6 example input 4";
Matcher matcher = p.matcher(input);
String output = matcher.replaceAll(matchResult -> "%s%s%s".formatted("number", matchResult.group(2), matchResult.group(1) ));

output 应该等于 number example input 6

matchResult.group(0) 是整个模式,所以组的索引从 1 开始


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接