Expresiones Regulares en Java

De ChuWiki
Saltar a: navegación, buscar

Veamos aquí algunos detalles de cómo usar expresiones regulares en java. No vamos a explicar con detalle las expresiones regulares, ya que en todos los lenguajes de programación son muy similares y usan sintaxis muy parecidas. Si no tienes una idea básica de cómo funcionan las expresiones regulares, quizás te interese echar un ojo a Introduccion a las expresiones regulares

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í ver unos ejemplos de cómo usarlas en Java en ciertos casos de interés.

Algunos ejemplos de expresiones regulares

Veamos algunos ejemplos concretos, algunos de los patrones más utilizados en nuestro código java

Expresión regular para fecha

Imaginemos la fecha en formato dd/mm/yyyy. Son grupos de dos cifras separadas por barras. En una expresión regular \d representa una cifra. El día pueden ser una o dos cifras, es decir \d{1,2], el mes igual y el año vamos a obligar que sean cuatro cifras exactamente \d{4}

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 comprueba si la cadena cumple con la expresión regular. Ten en cuenta que cuando en java metemos un caracter \ dentro de una cadena delimitada por "", debemos "escapar" esta \ con otra \, por ello todas nuestras \ en la expresión regular, se convierten en \\ en nuestro código java.

String regexp = "\\d{1,2}/\\d{1,2}/\\d{4}";

// Lo siguiente devuelve true
System.out.println(Pattern.matches(regexp, "11/12/2014"));
System.out.println(Pattern.matches(regexp, "1/12/2014"));
System.out.println(Pattern.matches(regexp, "11/2/2014"));


// Los siguientes devuelven false
System.out.println(Pattern.matches(regexp, "11/12/14"));  // El año no tiene cuatro cifras
System.out.println(Pattern.matches(regexp, "11//2014"));  // el mes no tiene una o dos cifras
System.out.println(Pattern.matches(regexp, "11/12/14perico"));  // Sobra "perico"

Supongamos que queremos que el mes se exprese como "ene", "feb", "mar", ... en vez de como un número. Cuando hay varias posibles cadenas válidas, en la expresión regular se ponen entre paréntesis y separadas por |. Es decir, algo como esto (ene|feb|mar|abr|may|jun|jul|ago|sep|oct|nov|dic). Si además nos da igual mayúsculas o minúsculas, justo delante ponemos el flag de case insensitive (?i) (la 'i' es de ignore case)

El siguiente código muestra un ejemplo completo de esto.

String literalMonthRegexp = "\\d{1,2}/(?i)(ene|feb|mar|abr|may|jun|jul|ago|sep|oct|nov|dic)/\\d{4}";

// Lo siguiente devuelve true
System.out.println(Pattern.matches(literalMonthRegexp, "11/dic/2014"));
System.out.println(Pattern.matches(literalMonthRegexp, "1/nov/2014"));
System.out.println(Pattern.matches(literalMonthRegexp, "1/AGO/2014"));   // Mes en mayúsculas
System.out.println(Pattern.matches(literalMonthRegexp, "21/Oct/2014"));  // Primera letra del mes en mayúsculas.

// Los siguientes devuelven false
System.out.println(Pattern.matches(literalMonthRegexp, "11/abc/2014"));   // abc no es un mes
System.out.println(Pattern.matches(literalMonthRegexp, "11//2014"));      // falta el mes
System.out.println(Pattern.matches(literalMonthRegexp, "11/jul/2014perico"));   // sobra perico

Expresión regular para DNI

En España existe el DNI (Documento Nacional de Identidad), últimamente también llamado NIF (Número de identificación fiscal), que lleva un número único y sirve para identificar a la persona. Este número son 8 cifras seguidas de una letra, que normalmente se escribe en mayúscula. Esta letra es una especie de checksum de las cifras anteriores, por lo que hay un algoritmo para validar que el número completo es correcto. Quedan excluidas las letras 'I', 'O' y 'U'. Las dos primeras por poder confundirse con uno y cero respectivamente. La tercera por algún extraño motivo.

Una expresión regular no va a realizar este checksum, pero sí nos puede ayudar a hacer una primera comprobación: 8 cifras y una letra mayúscula. La expresión regular puede ser así \d{8}[A-HJ-NP-TV-Z]

El siguiente código muestra un ejemplo completo de la expresión regular de DNI

String dniRegexp = "\\d{8}[A-HJ-NP-TV-Z]";

// Lo siguiente devuelve true
System.out.println(Pattern.matches(dniRegexp, "01234567C"));

// Lo siguiente devuelve faslse
System.out.println(Pattern.matches(dniRegexp, "01234567U")); // La U no es válida
System.out.println(Pattern.matches(dniRegexp, "0123567X")); // No tiene 8 cifras

Expresión regular para email

Antes de nada, no existe una expresión regular para email que sea 100% fiable, puesto que hay muchos formatos válidos de email y muy complejos. Aquí vamos a usar una expresión regular más o menos sencilla extraída de ese enlace : [^@]+@[^@]+\.[a-zA-Z]{2,}. Significa lo siguiente, un email válido está compuesto de :

  • [^@]+ cualquier caracter que no sea @ una o más veces seguido de
  • @ una @ seguido de
  • [^@]+ cualquier caracter que no sea @ una o más veces seguido de
  • \. un punto seguido de
  • [a-zA-Z]{2,} dos o más letras minúsculas o mayúsculas

Un ejemplo en código java de esta expresión regular

String emailRegexp = "[^@]+@[^@]+\\.[a-zA-Z]{2,}";

// Lo siguiente devuelve true
System.out.println(Pattern.matches(emailRegexp, "a@b.com"));
System.out.println(Pattern.matches(emailRegexp, "+++@+++.com"));

// Lo siguiente devuelve faslse
System.out.println(Pattern.matches(emailRegexp, "@b.com")); // Falta el nombre
System.out.println(Pattern.matches(emailRegexp, "a@b.c")); // El dominio final debe tener al menos dos letras


Extraer partes de la cadena

Una vez que vemos la forma de ver si una cadena cumple el patrón, podemos querer extraer parte de ese patrón, por ejemplo, las cifras de la fecha (día, mes y año). 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 \ delante.

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

extraer direcciones de email

El siguiente ejemplo extrae las direcciones de email existentes en un String, usando otra expresión regular de email distinta de la anterior (más restrictiva), pero extraída de la misma página.

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-]+)*)");
        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

Veamos el significado de la expresión regular, trozo por trozo

  • [a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+ debe empezar por alguno de los caracteres entre los corchetes ( _ - mayúscula, minúscula o número, extraños símbolos), una o más veces (el +)
  • @ este es fácil, una @
  • [a-zA-Z0-9-]+ Una o más letras, mayúsculas, minúsculas, dígitos o el guión.
  • (?:\\.[a-zA-Z0-9-]+)*) esta no es tan fácil, quiere decir que puede haber después 0 o más veces un punto seguido de una o más letras, algo como dominio.com.es. Veamos los trocitos
    • \\. un punto
    • [a-zA-Z0-9-]+ minúsculas, mayúsculas, dígitos o guión una o más veces.
    • (?: )* los dos trocitos anteriores (el punto y las una o más letras), se encierran entre paréntesis y se repiten 0 o más veces (el asterico del final). El ?: se pone para indicar que NO queremos extraer en nuestro código el trozo que está entre estos paréntesis. Recuerda que poner algo entre paréntesis en nuestra expresión regular es que vamos a querer extraer ese trozo en nuestro código. Si lo encerramos entre (?: ) queremos decir que ese trozo concreto no nos interesa y que lo ignore. Necesitamos los paréntesis solo para poner el * detrás.

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 decir, 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 de extraer el texto dentro de los tags html, 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 <>, tenga lo que tenga dentro 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 pueda 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 <.*> 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 patró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 pusiéramos el interrogante, este trozo de patrón cogería todo hasta el </c></nowwiki></code> final, excluyéndolo. Por último, el último <code><nowiki></.*?> casará sólo con el </a> y si no pusiéramos el interrogante, casaría con todo el resto de la cadena.

Finalmente, existe otra forma llamada possesive o posesiva. Funciona exactamente 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 "menor que", luego todo lo que no sea "mayor que" y finalmente coge el "mayor que", es decir, lo que sería un tag con su apertura < y cierre >
  • ([^<]*+) Va leyendo todo lo que no sea "menor que", es decir, lo que hay entre la apertura del tag encontrado antes y el principio del siguiente tag, presumiblemente el de cierre. Esta es la parte que nos interesa, por lo que la metemos entre paréntesis.
  • </[^>]*+> El "menor que", la /, todo lo que haya que no sea "mayor que" y el "mayor que". Es decir, otro cierre de tag.

De esta forma, los possessive se irían "comiendo" todo hasta encontrar un "mayor que" o "menor que", según el caso. En cualquier caso, se puede podría hacer exactamente lo mismo con greedy.

¿Para qué se usa este modo entonces?. Únicamente 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 "mayor 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.

Enlaces