grupos opcionales en expresiones regulares – java regex

Pregunta:


Respondiendo otra pregunta en SO, me surgió la interrogante de como lidiar con expresiones regulares cuando contienen grupos opcionales.

Por ejemplo, si quisiera capturar el número de teléfono y número favorito en el siguiente texto:

hola mi telefono es 12345678 y mi numero favorito es el 13

Usuaría una expresión como:

telefono[^d]*(d+).*numero favorito[^d]*(d+)

Si ambos datos fuesen opcionales, haría algo como:

(?:telefono[^d]*(d+))?.*(?:numero favorito[^d]*(d+))?

Pero esa expresión no funciona, ya que .* hace match de todo y los grupos opcionales quedan vacíos.

La única forma que he encontrado para especificar los caracteres entre ambos grupos para que sigan funcionando es con un negative lookahead de todas las cadenas que ocupo en los grupos:

(?:telefono[^d]*(d+))?(?:(?!telefono|numero favorito).)*(?:numero favorito[^d]*(d+))?

Si bien con eso ya se logra obtener los grupos opcionales, existen matchs posibles en los que no se ocupa ningún grupo. Además algo así no escalaría muy bien para muchos grupos.

¿Existe alguna alternativa?

Preguntado por: Klaimmore

Mariano

Multiples grupos, todos opcionales, diferenciando cada uno en el resultado

Todo lo que mencionaste en la pregunta tiene sentido, y es un buen análisis del problema. Pero se puede encarar de una forma más fácil. En vez de buscar coincidir con los dos números en 1 única coincidencia, conviene pensar en coincidencias independientes:

telefonoD*(d+)|numero favoritoD*(d+)

De esta forma, en cada coincidencia busca a uno o al otro, y te devolvería una coincidencia para el grupo 1 o el grupo 2 de acuerdo a cual corresponda. Llamamos a Matcher#find() mientras siga coincidiendo:

import java.util.regex.Matcher;
import java.util.regex.Pattern;
final String regex = "telefono\D*(\d+)|numero favorito\D*(\d+)";
final String texto = "hola mi telefono es 12345678 y mi numero favorito es el 13";

final Matcher matcher = Pattern.compile(regex).matcher(texto);

while (matcher.find()) {
    if (matcher.group(1) != null) {
        System.out.println("Tel: " + matcher.group(1));
    } else {
        System.out.println("Num: " + matcher.group(2));
    }
}


De todas formas, sé que tu pregunta apunta más a la teoría que la práctica. Si los grupos tienen que aparecer en ese orden en el texto, siendo igualmente opcionales, entonces, la forma de capturarlos sería agregando al texto intermedio (.*) dentro de la parte opcional. Es decir:

^(?:.*telefonoD*(d+))?(?:.*numero favoritoD*(d+))?

Recordemos que el motor de regex es goloso (greedy), por lo que para cada cuantificador, siempre intenta coincidir con lo más posible. En este caso significa que el (?:)? intenta con 1 antes que 0… Con eso nos garantizamos recorrer todo el string hasta encontrar una coincidencia (por ejemplo en .*telefonoD*(d+)), y recién tomarlo como opcional si esa parte no coincide.

import java.util.regex.Matcher;
import java.util.regex.Pattern;
final String regex = "^(?:.*telefono\D*(\d+))?(?:.*numero favorito\D*(\d+))?";
final String texto = "hola mi telefono es 12345678 y mi numero favorito es el 13";

final Matcher matcher = Pattern.compile(regex).matcher(texto);

if (matcher.find()) { // ← if redundante (siempre coincide)
    if (matcher.group(1) != null) {
        System.out.println("Tel: " + matcher.group(1));
    }
    if (matcher.group(2) != null) {
        System.out.println("Num: " + matcher.group(2));
    }
}
  • Hay que usar Pattern.DOTALL si se puede extender más allá de un salto de línea.

Si se pueden presentar en cualquier orden, solamente es necesario reemplazar los grupos sin captura (?:)? por inspecciones positivas (lookaheads) (?=)?.


Otra forma de tener múltiples grupos opcionales en orden es:

(?:telefonoD*(d+).*?)?(?:numero favoritoD*(d+).*?)?$

Lo que hace es que si coincide con uno de los grupos, sigue consumiendo lo menos posible (no goloso, lazy) con .*? hasta el próximo grupo, pero al mismo tiempo lo estoy obligando a recorrer todo el string hasta coincidir con el final $.

Fuente

Tags:,

Add a Comment

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *