Hacer coincidir texto de varias líneas con expresión regular

174

Estoy tratando de hacer coincidir un texto de varias líneas con Java. Cuando uso elPattern clase con el Pattern.MULTILINEmodificador, puedo hacer coincidir, pero no puedo hacerlo con(?m).

El mismo patrón con (?m) y usando String.matchesno parece funcionar.

Estoy seguro de que me falta algo, pero no tengo idea de qué. No soy muy bueno con las expresiones regulares.

Esto es lo que probé

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
fuente

Respuestas:

298

Primero, estás usando los modificadores bajo una suposición incorrecta.

Pattern.MULTILINEo (?m)le dice a Java que acepte las anclas ^y $que coincida al principio y al final de cada línea (de lo contrario, solo coinciden al inicio / final de toda la cadena).

Pattern.DOTALLo (?s)le dice a Java que también permita que el punto coincida con los caracteres de nueva línea.

En segundo lugar, en su caso, la expresión regular falla porque está utilizando el matches()método que espera que la expresión regular coincida con la totalidad cadena , lo que, por supuesto, no funciona, ya que quedan algunos caracteres después de (\\W)*(\\S)*haber coincidido.

Entonces, si simplemente está buscando una cadena que comienza con User Comments:, use la expresión regular

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

con la Pattern.DOTALLopción:

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

ResultString contendrá el texto después User Comments:

Tim Pietzcker
fuente
Estoy tratando de encontrar un patrón que coincida con cualquier cadena que comience con "Comentarios del usuario:". Después de estos "Comentarios de usuario:" es algo que un usuario ingresa en un área de texto y, por lo tanto, puede contener cualquier cosa , incluso nuevas líneas. Parece que necesito aprender mucho en expresiones regulares ...
Nivas
2
Esto funciona (¡gracias!) Probé el patrón (?s)User Comments:\s*(.*). De la respuesta de @Amarghosh obtuve el patrón User Comments: [\\s\\S]*. Entre estos, ¿hay una forma mejor o recomendada o son solo dos formas diferentes de hacer lo mismo?
Nivas
3
Ambos significan lo mismo; [\s\S]es un poco más explícita ( "coincidirá con cualquier carácter que puede ser un espacio en blanco o no está en blanco"), .es más fácil de leer, pero hay que buscar la (?s)o DOTALLmodificador con el fin de averiguar si los saltos de línea se incluyen o no. Preferiría .con el Pattern.DOTALLconjunto de banderas (esto es más fácil de leer y recordar que (?s)en mi opinión. Debería usar lo que le
resulte
.*con DOTALLes más legible. Utilicé el otro para mostrar que el problema está en las diferencias entre str.matches y matcher.find y no en las banderas. +1
Amarghosh
Prefiero .*con Pattern.DOTALL, pero tendré que ir con (? S) porque tengo que usar String.matches.
Nivas
42

Esto no tiene nada que ver con la bandera MULTILINE; Lo que estás viendo es la diferencia entre los métodos find()y matches(). find()tiene éxito si se puede encontrar una coincidencia en cualquier parte de la cadena de destino , mientras se matches()espera que la expresión regular coincida con la cadena completa .

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

Además, MULTILINEno significa lo que crees que hace. Muchas personas parecen llegar a la conclusión de que debe usar esa bandera si su cadena de destino contiene nuevas líneas, es decir, si contiene varias líneas lógicas. He visto varias respuestas aquí sobre SO para ese efecto, pero de hecho, todo lo que hace la bandera es cambiar el comportamiento de los anclajes, ^y $.

Normalmente ^coincide con el comienzo de la cadena de destino y $coincide con el final (o antes de una nueva línea al final, pero lo dejaremos de lado por ahora). Pero si la cadena contiene nuevas líneas, puede elegir ^y $coincidir al comienzo y al final de cualquier línea lógica, no solo al inicio y al final de toda la cadena, configurando el indicador MULTILINE.

Así que olvídate de lo que MULTILINE significa y sólo recuerda lo que hace : cambia el comportamiento de la ^y $anclajes. DOTALLOriginalmente, el modo se llamaba "una sola línea" (y todavía tiene algunos sabores, incluidos Perl y .NET), y siempre ha causado una confusión similar. Somos afortunados de que los desarrolladores de Java hayan elegido el nombre más descriptivo en ese caso, pero no había una alternativa razonable para el modo "multilínea".

En Perl, donde comenzó toda esta locura, admitieron su error y se deshicieron de los modos "multilínea" y "línea única" en las expresiones regulares de Perl 6. En otros veinte años, tal vez el resto del mundo habrá seguido su ejemplo.

Alan Moore
fuente
55
Es difícil de creer que usaron el nombre del método "# coincide" para significar "coincide con todos"
yikes
@ alan-moore Lo siento, pero esto es correcto [necesita dormir más :)]
Raymond Naseef
22

str.matches(regex) se comporta como el Pattern.matches(regex, str) que intenta hacer coincidir toda la secuencia de entrada con el patrón y devuelve

truesi, y sólo si, la totalidad de la secuencia de entrada coincida con el patrón de este matcher

Mientras que matcher.find() intenta encontrar la siguiente subsecuencia de la secuencia de entrada que coincida con el patrón y devuelve

truesi, y solo si, una subsecuencia de la secuencia de entrada coincide con el patrón de este emparejador

Por lo tanto, el problema es con la expresión regular. Intenta lo siguiente.

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

En resumen, la (\\W)*(\\S)*porción en su primera expresión regular coincide con una cadena vacía, lo que *significa cero o más ocurrencias y la cadena real coincidente es User Comments:y no la cadena completa como cabría esperar. El segundo falla cuando intenta hacer coincidir toda la cadena, pero no puede \\Wcoincidir con un carácter que no sea de palabra, es decir, [^a-zA-Z0-9_]y el primer carácter es Tun carácter de palabra.

Amarghosh
fuente
Quiero hacer coincidir cualquier cadena que comience con "Comentarios del usuario", y la cadena también puede contener nuevas líneas. Entonces usé el patrón User Comments: [\\s\\S]*y esto funcionó. (¡gracias!) De la respuesta de @Tim obtuve el patrón User Comments:(.*), esto también está bien. Ahora, ¿hay alguna forma recomendada o mejor entre estas, o estas son solo dos formas de hacer lo mismo?
Nivas
@Nivas No creo que haya ninguna diferencia en cuanto al rendimiento; pero creo que (.*)junto con la DOTALLbandera es más obvio / legible que([\\s\\S]*)
Amarghosh
Esta es la mejor respuesta ... proporciona acceso tanto al código Java como a las opciones de Cadena de patrones, para la capacidad MultiLine.
GoldBishop
0

La bandera multilínea le dice a regex que haga coincidir el patrón con cada línea en lugar de la cadena completa para sus propósitos, un comodín será suficiente.

Yehuda Schwartz
fuente