Expresiones Regulares en Java

De ChuWiki
Saltar a: navegación, buscar

Contenido


Una pequeña introducción

A veces es necesario que nuestro código analice una cadena de caracteres para buscar algo o bien para comprobar que cumple un determinado patrón. Por ejemplo, si pedimos por teclado una fecha dd/mm/yy, necesitamos comprobar si la cadena leída cumple ese patrón: dos cifras, una barra, dos cifras, otra barra y otras dos cifras.

Las expresiones regulares de java nos ayudan a hacer estos análisis. No vamos a dar aquí una lista detallada de todas las posibilidades, pero sí una pequeña introducción.

Para el ejemplo anterior, si queremos comprobar que una cadena leída por teclado cumple ese patrón, podemos usar la clase Pattern. A la clase Pattern le decimos el patrón que queremos que cumpla nuestra cadena y nos dice si la cumple o no.

El siguiente ejemplo comrpueba si la cadena cumple

String fechaLeida = "11/22/07";
// Lo siguiente devuelve true
boolean cumplePatron = Pattern.matches("\\d\\d/\\d\\d/\\d\\d", fechaLeida);

fechaLeida="2/3/4";
// Lo siguiente devuelve false
cumplePatron = Pattern.matches("\\d\\d/\\d\\d/\\d\\d", fechaLeida);

En el primer caso, fechaLeida tiene una fecha en el formato que la esperamos -aunque es incorrecta, no hay mes 22-. El patrón es el primer parámetro del método matches(), ese que tiene tantas \ y tantas d. En un patrón de estos, \\d representa un dígito. Así, nuestro patrón dice que debe haber dos dígitos, una /, dos dígitos, una barra y otros dos dígitos.

El primer matches(), con fechaLeida="11/22/07" cumple el patrón, así que matches() devuelve true.

En el segundo caso, la fechaLeida sólo tiene un dígito en cada caso, así que no cumple el patrón y devulve false.

Si queremos algo más sofisticado, para que se admitan los días y meses con uno o dos dígitos, podemos usar el siguiente patrón

String fechaLeida = "11/22/07";
// devuelve true
cumplePatron = Pattern.matches("\\d{1,2}/\\d{1,2}/\\d{1,2}", fechaLeida);

fechaLeida="2/3/4";
// devuelve true
cumplePatron = Pattern.matches("\\d{1,2}/\\d{1,2}/\\d{1,2}", fechaLeida);

Ahora, la expresión \\d{1,2} indica un dígito entre 1 y 2 veces, es decir, uno o dos dígitos.

Hay muchas más opciones. Una vez que entendemos el concepto, no te costará entender la lista completa en la API de Pattern.

Extraer partes de la cadena

Una vez que vemos la forma de ver si una cadena cumple el patrón dd/mm/yy, podemos querer extraer esas cifras. Nuevamente las expresiones regulares de java nos ayudan. Cambiemos el ejemplo. Queremos extraer los sumandos y el resultado de una cadena así "xxxx+yyyy=zzzzz" donde x, y y z representan dígitos y pueden ser en cualquier número.

Con \\d+ indicamos uno o más dígitos. La expresión regular para ver si una cadena cumple ese patrón puede ser "\\d+\\+\\d+=\\d+". Puesto que el + tiene un sentido especial en los patrones -indica uno o más-, para ver si hay un + en la cadena, tenemos que "escaparlo", por eso el \\+

Las partes que queramos extraer, debemos meterlas entre paréntesis. Así, la expresión regular quedaría "(\\d+)\\+(\\d+)=(\\d+)".

El código para extraer los sumandos y el resultado puede ser así

// La cadena a analizar
String cadena = "23+12=35";

// Obtenemos un Pattern con la expresión regular, y de él
// un Matcher, para extraer los trozos de interés.
Pattern patron = Pattern.compile("(\\d+)\\+(\\d+)=(\\d+)");
Matcher matcher = patron.matcher(cadena);

// Hace que Matcher busque los trozos.
matcher.find();

// Va devolviendo los trozos. El primer paréntesis es el 1,
// el segundo el 2 y el tercero el 3
System.out.println(matcher.group(1));
System.out.println(matcher.group(2));
System.out.println(matcher.group(3));

// La salida de este programa es
// 23
// 12
// 35

Buscar a lo largo de la cadena

Supongamos la siguiente cadena de text <a>uno</a><b>dos</b><c>tres</c> y que queremos extraer usando expresiones regulares los trozos que hay entre los tags <a>, <b> y <c>, es decir, "uno", "dos" y "tres".

No es necesario que la cadena coincida exactamente con el patrón en su longitud total. Es posible tener una cadena larga, por ejemplo <a>uno</a><b>dos</b><c>tres</c> y un patrón que sólo coincida con parte de la cadena, por ejemplo, <[^>]*>([^<]*)</[^>]*>. Es decir

  • <[^>]*> Este trozo busca los tags encerrados entre los símbolos mayor y menor. El [^>] indica cualquier caracter que no sea >. El * detrás indica que puede estar 0 o más veces.
  • ([^<]*) Busca lo que hay entre tags, es decir, cualquier caracter que no sea "menor que". Como es lo que queremos extraer de la cadena, lo ponemos entre paréntesis, de forma que el método find() será lo que nos vaya devolviendo en sucesivas llamadas.
  • </[^>]*> Buscamos la finalización del tag, es decir, un menor qué, seguido de una barra / y todos los caracteres que no sean "mayor qué".

Esa expresión regular extraería de la cadena el "uno" en una primera llamada a find(). Tendríamos que hacer un bucle para repetir tantas veces como sea necesario. El bucle sería como el siguiente

String cadena = "<a>uno</a><b>dos</b><c>tres</c>";
Pattern pattern1 = Pattern.compile("<[^>]*>([^<]*)</[^>]*>");
Matcher matcher1 = pattern1.matcher(cadena);

for (int i = 0; i < 1; i++) {
   while (matcher1.find()) {
      System.out.println(matcher1.group(1));
   }
}

Es decir, una vez construído el Matcher, vamos haciendo sucesivas llamadas a find() para obtener el contenido de cada uno de los tags. Como en nuestro patrón sólo hay un paréntesis, el group() siempre será el 1.

Un ejemplo para extraer direcciones de email

El siguiente ejemplo extrae las direcciones de email existentes en un String


package com.chuidiang.ejemplos;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ExtractorEmails {

    public static void main(String[] args) {
        String entrada = "<p>hola@pedro.com</p><br>\n";
        entrada += "kk@tres.tris///pepe@eso.es";

        Pattern limpiar = Pattern
                .compile("([_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,}))");
        Matcher buscar = limpiar.matcher(entrada);
        while (buscar.find())
            System.out.println(buscar.group(1));
    }
}

La salida de este código será

hola@pedro.com
kk@tres.tris
pepe@eso.es

La expresión regular que hemos usado para el correo [_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,}) la he extraído de aquí http://www.mkyong.com/regular-expressions/how-to-validate-email-address-with-regular-expression/ Básicamente dice que el correo

  • [_A-Za-z0-9-]+ debe empezar por alguno de los caracteres entre los corchetes ( _ - mayúscula, minúscula o número), una o más veces (el +)
  • (\\.[_A-Za-z0-9-]+)* seguido de 0 o más (el * del final) de cualquiera de los caracteres entre paréntesis, que son los mismos de antes más el punto, pero no puede haber dos puntos seguidos (el + detrás de los corchetes indica que después de un punto debe haber al menos una letra o cifra. Es decir, este chorizo representa que puede haber un punto seguido de al una o más letras/dígitos, todas las veces que queramos.
  • @ este es fácil, una @
  • [A-Za-z0-9]+ Una o más letras, mayúsculas, minúsculas o dígitos
  • (\\.[A-Za-z]{2,}) Un punto y al menos dos letras mayúsculas o minúsculas, sin dígitos.

Greedy, reluctant y possesive

Cuando java intenta encajar un patrón en una cadena, puede haber varias posibles coincidencias. Por ejemplo, si la cadena es "aaaaa" y el patrón es "a*", podría considerarse que el patrón se cumple 5 veces (es decir, el * se tomaría como 1 ocurrencia y a* se interpretaría como "a"), o bien podría interpretarse que el patrón sólo se cumple una vez, es deicr, el * sería 5 ocurrencias y "a*" sería "aaaaa". Java nos permite elegir cómo queremos que se haga.

Una forma de resolver el ejemplo del punto anterior sería con el patrón <.*>(.*)</.*>, es decir, buscamos

  • <.*> Una apertura de tag
  • (.*) Lo que hay entre tags
  • </.*> El cierre de tag

Y el código java sería

String cadena = "<a>uno</a><b>dos</b><c>tres</tres>";

Pattern pattern1 = Pattern.compile("<.*>(.*)</.*>");

Matcher matcher1 = pattern1.matcher(cadena);
while (matcher1.find()) {
   System.out.println("1 " + matcher1.group(1));
}

Estamos buscando cualquier cosas entre <.*> y </.*>, es decir, entre cualquier tag delimintado por < y >, tenga lo que tenga dentro y </ y >. Nos quedamos con lo que hay entre ellos (el (.*)). Metiendo el Matcher correspondiente en un bucle con find(), esperamos encontrar las tres cadenas buscadas.

Pero esto no funciona, sólo nos da la última. ¿Por qué?. La búsqueda empieza con un <.*>, es decir, busca un < (el que está al principio de la cadena) y luego va saltando caracteres, todos los que puede, hasta que encuentre algo que puda casar con el resto del matcher. Y lo que encuentra es que el primero <.*> casa perfectamente con <a>uno</a><b>dos</b><c>. Luego el (.*) casa con el "tres" (y es lo que nos va a devolver la llamada al group(1) y el último </.*> casa con </c>.

Este comportamiento, que no es el que queremos, se conocd como "greedy" (glotón), en el que cada uno de los trozos del patrón que ponemos trata de coger lo máximo posible de la cadena. El primer trozo del patrón <.*> casa perfectamente con el primero <a>, pero también con <a>uno</a>, con <a>uno</a><b>, con .... y con la cadena completa <a>uno</a><b>dos</b><c>tres</c>. El comportamiento greedy trata de coger lo máximo posible, pero siempre intentando que el patrón completo se cumpla. Por ello, el primero <nowikwi><.*></nowiki> coge <a>uno</a><b>dos</b><c>, que es lo máximo que puede coger haciendo que el patrón completo se cumpla.

Otro posible comportamiento es "reluctant" o perezoso. Este comportamiento es el contrario de greedy. Tratará de coger lo menos posible, pero siempre intentando que se cumpla el pattrón. Para este comportamiento, añadimos un ? detrás. Así, el siguiente código

Pattern pattern2 = Pattern.compile("<.*?>(.*?)</.*?>");

Matcher matcher2 = pattern2.matcher(cadena);
while (matcher2.find()) {
   System.out.println("2 " + matcher2.group(1));
}

funcionará según lo esperado, ya que <.*?> intentará coger lo menos posible que cumpla el patrón, es decir, la <a>. El (.*?) hará lo mismo, es decir el "uno" (si no pusieramos el interrogante, este trozo de patrón cogería todo hasta el </c></nowwiki> final, excluyéndolo. Por último, el último <nowiki></.*?> casará sólo con el </a> y si no pusieramos el interrogante, casaría con todo el resto de la cadena.

Finalmente, existe otra forma llamada "possesive" o posesiva. Funciona exacamtente igual que greedy (es decir, trata de coger lo máximo posible), pero a diferencia de greedy no se preocupa de hacer que se cumpla el patrón. Para este modo se pone un más en vez de un interrogante y el patrón quedaría <.*+>(.*+)</.*+>. No funcionaría nunca ni encontraría nada, porque el primer < casaría con el primer < de la cadena y el .*+ se "comería" el resto de la cadena hasta el final. La forma correcta de usar este cuantificador para este tipo de cadena podría ser <[^>]*+>([^<]*+)</[^>]*+> de forma que

  • <[^>]*+> Busca el "mayor que", luego se salta todos los caracteres que no sean "menor que" y busca el "menor que", es decir, lo que sería un tag.
  • ([^<]*+) Va leyendo todo lo que no sea "menor que", es decir, lo que hay entre la apertura del tag y el principio del cierre.
  • </[^>]*+> El "menor que", la /, todo lo que haya que no sea "menor que" y el "menor que".

De esta forma, los possessive se irían "comiendo" todo hasta encontrar un "mayor que" o "menor que", según el caso.

¿Para qué se usa este modo entonces?. Unicamente por motivos de eficiencia. Si en la cadena hay un trozo que queramos quitar y que podamos distinguir con una expresión regular, podemos ponerlo con este modo possesive. De esta forma, el possesive se comerá directamente ese trozo de cadena y no perderá el tiempo tratando de hacer casar ese trozo con el patrón de alguna u otra forma. En el ejemplo anterior, si la cadena fuera larga y no hubiera ningún "menor que", el primer trozo de [^>]*+ se "comería" toda la cadena dando fallo en la búsqueda directamente, mientras que los otros dos cuantificadores (greedy y reluctant), tras ver el fallo, tratarían de retroceder en la cadena a ver si "comiendo" más o menos caracteres pueden hacerla "casar" de alguna forma.