Posso substituir grupos no Java regex?

95

Eu tenho esse código e quero saber se posso substituir apenas grupos (não todos os padrões) no Java regex. Código:

 //...
 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.

    }
wokena
fonte
6
Você pode esclarecer sua pergunta, como talvez dar a saída esperada para essa entrada?
Michael Myers

Respostas:

125

Use $n(onde n é um dígito) para se referir às subsequências capturadas em replaceFirst(...). Presumo que você queira substituir o primeiro grupo pela string literal "número" e o segundo grupo pelo valor do primeiro grupo.

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
    String output = m.replaceFirst("number $3$1");  // number 46
}

Considere (\D+)para o segundo grupo em vez de (.*). *é um matcher ganancioso e, a princípio, consumirá o último dígito. O combinador terá então que retroceder quando perceber que a final (\d)não tem nada a corresponder, antes que possa corresponder ao dígito final.

Chadwick
fonte
7
Teria sido bom se você tivesse postado um exemplo de saída
winklerrr
6
Isso funciona na primeira partida, mas não funcionará se houver muitos grupos e você estiver iterando sobre eles com um tempo (m.find ())
Hugo Zaragoza
1
Concordo com o Hugo, essa é uma maneira terrível de implementar a solução ... Por que diabos essa é a resposta aceita e não a resposta do acdcjunior - que é a solução perfeita: pequena quantidade de código, alta coesão e baixo acoplamento, muito menos chance (se não houver chance) de efeitos colaterais indesejados ... suspiro ...
FireLight
Esta resposta não é válida atualmente. O m.replaceFirst("number $2$1");deveria serm.replaceFirst("number $3$1");
Daniel Eisenreich
52

Você pode usar Matcher#start(group)e Matcher#end(group)para construir um método de substituição genérico:

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, "!!!"));
}

Verifique a demonstração online aqui .

acdcjunior
fonte
1
Esta realmente deve ser a resposta aceita - é a solução mais completa e "pronta para usar" sem introduzir um nível de acoplamento ao código que o acompanha. Embora eu recomende alterar os nomes dos métodos de um deles. À primeira vista, parece uma chamada recursiva no primeiro método.
FireLight de
Oportunidade de edição perdida. Retome a parte sobre a chamada recursiva, não analisei o código corretamente. As sobrecargas funcionam bem juntas
FireLight
23

Desculpe bater em um cavalo morto, mas é meio estranho que ninguém apontou isso - "Sim, você pode, mas isso é o oposto de como você usa a captura de grupos na vida real".

Se você usar o Regex da maneira que deve ser usado, a solução é tão simples quanto esta:

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

Ou, conforme indicado por shmosel abaixo,

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

... já que na sua regex não há nenhuma boa razão para agrupar os decimais.

Você não costuma usar grupos de captura nas partes da corda que deseja descartar , você os usa na parte da corda que deseja manter .

Se você realmente deseja grupos que deseja substituir, o que provavelmente deseja é um mecanismo de modelagem (por exemplo, bigode, ejs, StringTemplate, ...).


Como um aparte para os curiosos, mesmo os grupos que não capturam em regexes estão lá apenas para o caso de o mecanismo de regex precisar deles para reconhecer e pular o texto variável. Por exemplo, em

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

você precisa deles se sua entrada puder ser semelhante a "abcabc capture me bcdbcd" ou "abc capture me bcd" ou mesmo apenas "capture me".

Ou, dito de outra forma: se o texto é sempre o mesmo e você não o captura, não há razão para usar grupos.

Yaro
fonte
1
Os grupos de não captura são desnecessários; \d(.*)\dserá suficiente.
shmosel
1
Eu não entendo o $11aqui. Por que 11?
Alexis
1
@Alexis - Esta é uma peculiaridade do java regex: se o grupo 11 não foi definido, java interpreta $ 11 como $ 1 seguido por 1.
Yaro
9

Adicione um terceiro grupo adicionando parênteses ao redor .*, então substitua a subsequência por "number" + m.group(2) + "1". por exemplo:

String output = m.replaceFirst("number" + m.group(2) + "1");
mkb
fonte
4
Na verdade, o Matcher suporta o estilo de referência $ 2, então m.replaceFirst ("número $ 21") faria a mesma coisa.
Michael Myers
Na verdade, eles não fazem a mesma coisa. "number$21"funciona e "number" + m.group(2) + "1"não funciona .
Alan Moore
2
Parece que number$21iria substituir o grupo 21, não o grupo 2 + a string "1".
Fernando M. Pinheiro
Esta é a concatenação de string simples, certo? por que precisamos chamar replaceFirst?
Zxcv Mnb
2

Você pode usar os métodos matcher.start () e matcher.end () para obter as posições do grupo. Assim, usando essas posições, você pode facilmente substituir qualquer texto.

Ydanneg
fonte
1

substitua os campos de senha da entrada:

{"_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, "**"));
  }
capricho
fonte
0

Aqui está uma solução diferente, que também permite a substituição de um único grupo em várias correspondências. Ele usa pilhas para reverter a ordem de execução, de forma que a operação da string possa ser executada com segurança.

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();       
}
Jonas_Hess
fonte