Corresponder texto de múltiplas linhas usando expressão regular

174

Estou tentando combinar um texto de várias linhas usando java. Quando uso a Patternclasse com o Pattern.MULTILINEmodificador, consigo fazer a correspondência, mas não consigo fazer isso com(?m).

O mesmo padrão com (?m)e usando String.matchesparece não funcionar.

Tenho certeza de que estou perdendo alguma coisa, mas não faço ideia do que. Não sou muito bom em expressões regulares.

Isto é o que eu tentei

String test = "User Comments: This is \t a\ta \n test \n\n message \n";

String pattern1 = "User Comments: (\\W)*(\\S)*";
Pattern p = Pattern.compile(pattern1, Pattern.MULTILINE);
System.out.println(p.matcher(test).find());  //true

String pattern2 = "(?m)User Comments: (\\W)*(\\S)*";
System.out.println(test.matches(pattern2));  //false - why?
Nivas
fonte

Respostas:

298

Primeiro, você está usando os modificadores sob uma suposição incorreta.

Pattern.MULTILINEou (?m)diz ao Java para aceitar as âncoras ^e $corresponder no início e no final de cada linha (caso contrário, elas corresponderão apenas no início / fim de toda a cadeia).

Pattern.DOTALLou (?s)diz ao Java para permitir que o ponto corresponda também a caracteres de nova linha.

Segundo, no seu caso, a regex falha porque você está usando o matches()método que espera que a regex corresponda a toda a cadeia - o que obviamente não funciona, pois existem alguns caracteres restantes após a (\\W)*(\\S)*correspondência.

Portanto, se você está simplesmente procurando por uma sequência que comece com User Comments:, use o regex

^\s*User Comments:\s*(.*)

com a Pattern.DOTALLopção:

Pattern regex = Pattern.compile("^\\s*User Comments:\\s+(.*)", Pattern.DOTALL);
Matcher regexMatcher = regex.matcher(subjectString);
if (regexMatcher.find()) {
    ResultString = regexMatcher.group(1);
} 

ResultString conterá o texto depois User Comments:

Tim Pietzcker
fonte
Estou tentando encontrar um padrão que corresponda a qualquer sequência que comece com "Comentários do usuário:". Depois disso, "Comentários do usuário:" é algo que um usuário insere em uma área de texto e, portanto, pode conter qualquer coisa - até mesmo novas linhas. Parece que eu preciso de aprender muito em regex ...
Nivas
2
Isso funciona (obrigado!) Eu tentei o padrão (?s)User Comments:\s*(.*). A partir da resposta de @Amarghosh, obtive o padrão User Comments: [\\s\\S]*. Entre essas, existe uma maneira melhor ou recomendada ou existem apenas duas maneiras diferentes de fazer o mesmo?
Nivas
3
Ambos significam o mesmo; [\s\S]é um pouco mais explícito ("corresponde a qualquer caractere que seja espaço em branco ou não em branco"), .é mais fácil de ler, mas você precisa procurar o modificador (?s)ou DOTALLpara descobrir se as novas linhas estão incluídas ou não. Eu prefiro .com o Pattern.DOTALLconjunto de sinalizador (isso é mais fácil de ler e se lembrar do que (?s)na minha opinião, você deve usar o que você se sinta mais confortável..
Tim Pietzcker
.*com DOTALLé mais legível. Eu usei o outro para mostrar que o problema está nas diferenças entre str.matches e matcher.find e não nos sinalizadores. +1
Amarghosh
Eu prefiro .*com Pattern.DOTALL, mas vou ter que ir com (?) Porque preciso usar String.matches.
Nivas
42

Isso não tem nada a ver com a bandeira MULTILINE; o que você está vendo é a diferença entre os métodos find()e matches(). find()terá êxito se uma correspondência puder ser encontrada em qualquer lugar da sequência de destino , enquanto matches()espera que o regex corresponda à sequência inteira .

Pattern p = Pattern.compile("xyz");

Matcher m = p.matcher("123xyzabc");
System.out.println(m.find());    // true
System.out.println(m.matches()); // false

Matcher m = p.matcher("xyz");
System.out.println(m.matches()); // true

Além disso, MULTILINEnão significa o que você pensa que faz. Muitas pessoas parecem chegar à conclusão de que você precisa usar esse sinalizador se a sequência de destino contiver novas linhas - ou seja, se contiver várias linhas lógicas. Eu já vi várias respostas aqui no SO para esse efeito, mas, na verdade, tudo o que a flag faz é alterar o comportamento das âncoras ^e $.

Normalmente ^corresponde ao início da string de destino e $corresponde ao final (ou antes de uma nova linha no final, mas vamos deixar isso de lado por enquanto). Mas se a sequência contiver novas linhas, você poderá escolher ^e $corresponder no início e no final de qualquer linha lógica, não apenas no início e no final de toda a sequência, configurando o sinalizador MULTILINE.

Portanto, esqueça o que MULTILINE significa e lembre-se do que ele faz : altera o comportamento das âncoras ^e $. DOTALLO modo foi originalmente chamado de "linha única" (e ainda existe em alguns tipos, incluindo Perl e .NET), e sempre causou confusão semelhante. Temos a sorte de que os desenvolvedores Java tenham o nome mais descritivo nesse caso, mas não havia alternativa razoável para o modo "multilinha".

No Perl, onde toda essa loucura começou, eles admitiram seu erro e se livraram dos modos "multilinha" e "linha única" nas expressões regulares do Perl 6. Em outros vinte anos, talvez o resto do mundo tenha seguido o exemplo.

Alan Moore
fonte
5
Difícil de acreditar que eles usaram o nome do método "#matches" para significar "corresponde a todos" yikes
rogerdpack
@ alan-moore Desculpe, mas estou certo de que isso esteja correto [preciso dormir mais :)]
Raymond Naseef
22

str.matches(regex) se comporta como o Pattern.matches(regex, str) que tenta corresponder toda a sequência de entrada ao padrão e retorna

truese, e somente se, toda a sequência de entrada corresponder ao padrão deste

Considerando que as matcher.find() tentativas de encontrar a próxima subsequência da sequência de entrada que corresponde ao padrão e retornam

truese, e somente se, uma subsequência da sequência de entrada corresponder ao padrão deste

Assim, o problema está no regex. Tente o seguinte.

String test = "User Comments: This is \t a\ta \ntest\n\n message \n";

String pattern1 = "User Comments: [\\s\\S]*^test$[\\s\\S]*";
Pattern p = Pattern.compile(pattern1, Pattern.MULTILINE);
System.out.println(p.matcher(test).find());  //true

String pattern2 = "(?m)User Comments: [\\s\\S]*^test$[\\s\\S]*";
System.out.println(test.matches(pattern2));  //true

Portanto, em resumo, a (\\W)*(\\S)*parte do seu primeiro regex corresponde a uma sequência vazia, pois *significa zero ou mais ocorrências, e a sequência real correspondida é User Comments:e não a sequência toda, como seria de esperar. O segundo falha quando tenta corresponder a toda a cadeia, mas não pode \\Wcorresponder a um caractere que não seja da palavra, ou seja, [^a-zA-Z0-9_]e o primeiro caractere é Tum caractere da palavra.

Amarghosh
fonte
Quero corresponder a qualquer sequência que comece com "Comentários do usuário" e a sequência também pode conter novas linhas. Então eu usei o padrão User Comments: [\\s\\S]*e isso funcionou. (obrigado!) Pela resposta do @Tim, obtive o padrão User Comments:(.*), isso também está correto. Agora, existe uma maneira recomendada ou melhor entre essas ou apenas duas maneiras de fazer a mesma coisa?
Nivas
@ Nivas Acho que não haveria diferença no desempenho; mas acho que (.*), juntamente com DOTALLbandeira é mais óbvio / legível do que([\\s\\S]*)
Amarghosh
Esta é a melhor resposta .... fornece acesso às opções de código Java e String padrão, para o recurso MultiLine.
precisa saber é o seguinte
0

O sinalizador de múltiplas linhas informa ao regex para corresponder o padrão a cada linha, em oposição a toda a cadeia de caracteres para os seus propósitos, um curinga será suficiente.

Yehuda Schwartz
fonte