Tutorial Java RegExp

Sep 2020

Las expresiones regulares son un gran aliado a la hora de buscar y reemplazar contenido textual que simplifica enormemente la búsqueda y sustitución de términos variables.

Qué son las expresiones regulares

Las expresiones regulares o simplemente regex son un gran aliado a la hora de buscar y reemplazar contenido textual. Puede decirse que son el gran subestimado en la edición de texto, puesto que aun ofreciendo una capacidad enorme a la hora de buscar situaciones variables, pocos programadores las utilizan por la complejidad de definir estas expresiones. Sin embargo, su uso es mucho más fácil de lo que parece, sobre todo si se compara con el tiempo que se requiere escribir un analizador léxico o una gramática como podría hacerse con ANTLR, por ejemplo. Además, si el programador se familiariza con éstas, podrá hacer uso de toda una batería de comandos del sistema operativo que permiten buscar y reemplazar contenido con ellas. La forma en la que se escriben las expresiones regulares es prácticamente un estándar, así que el usuario podrá usarlas en muchos ámbitos distintos.

API de Java para las expresiones regulares

En las siguientes líneas se describe cómo escribir y ejecutar expresiones regulares aprovechando el API de Java para poder realizar procesos sobre entradas de datos, reglas de validación, o parsers específicos que frecuentemente se dan en las tareas ordinarias del programador.

El API de Java para expresiones regulares está incluido de base en el JDK y por tanto en el JRE sin que deba añadir ninguna dependencia adicional. Su punto de partida es la construcción de un objeto Pattern del paquete java.util.regex. Pattern tiene un método estático compile(«…») por el cual se obtiene una instancia, dado un String que contiene la expresión regular. Este objeto debe reutilizarse entre usos de la misma expresión regular, para evitar incurrir en tiempos innecesarios al compilar una y otra vez la misma expresión.

Así que una vez compilada, el siguiente paso es aplicar la expresión sobre una entrada de texto a través del método del objeto matcher(«…»). Este método devuelve un objeto Matcher que agrupa todos los resultados que haya podido producir la aplicación de la expresión regular (regex) sobre la entrada. Obviamente este objeto no podrá reutilizarse para entradas de datos diferentes. El primer y más importante resultado que ofrece Matcher es si la regex ha sido válida o no para la entrada, esto es ofrecido por el método boolean find(). Si la respuesta es cierta, mediante el método group(int), el programador puede obtener el extracto de la entrada que ha sido compatible con cada uno de los grupos definidos en la expresión regular. El primer grupo es el 1, el segundo el 2, etc…

Véase como encajan todas las piezas del API de Java, y posteriormente se describirá como definir complejas regexps. Imagínese un caso sencillo en donde se desea comprobar si una entrada contiene la palabra hola, al igual que se realizaría con un String.contains().

Pattern pat = Pattern.compile("hola");
String input = "la caracola dice hola a la paloma";
Matcher mat = pat.matcher(input);
if (mat.find()) {
    System.out.println("regex encontrada");
} else {
    System.err.println("regex NO encontrada");
}

La anterior expresión regular hola permite buscar la palabra dentro del contenido alojado en la variable input. Esta expresión es muy simple ya que no aporta nada variable, sólo un literal.

Sin embargo, supóngase que se desea escribir otra expresión que permita obtener el nombre de los sujetos de la entrada para saber quién saluda a quién. En este caso se necesita que la expresión acepte entradas variables y además extraiga quiénes son los actores que intervienen en el saludo. El siguiente fragmento escribe una regex que permite precisamente eso, añadir dos partes variables a la expresión y definir dos grupos para obtener los sujetos.

String regex = "la (\\w+) dice hola a la (\\w+)";
Pattern pat = Pattern.compile(regex);
String input = "la caracola dice hola a la paloma";
Matcher mat = pat.matcher(input);
if (mat.find()) {
    System.out.println("regex encontrada");
    System.out.println("Sujeto activo :"+mat.group(1));
    System.out.println("Sujeto pasivo :"+mat.group(2));
} else {
    System.err.println("regex NO encontrada");
}

Esta nueva expresión es más larga y define dos partes variables, o grupos, identificadas con paréntesis que contienen los símbolos \\w+. Java intentará comparar la entrada con la expresión y verá que el término caracola encaja en la definición del grupo 1, mientras que paloma encaja en el segundo grupo. De ahí que se puedan obtener éstos usando mat.group(1), o 2 en el segundo.

Para ello, debe definirse qué significan esos símbolos entre paréntesis. La doble barra indica que lo que se desea escribir en la expresión es precisamente una contra-barra. Se debe escribir dos veces en Java, puesto que si no coincide con el carácter de escape, como el usado con \n para indicar el fin de línea. Sin embargo en este caso, lo que realmente se desea escribir es una contra barra \.

Lo siguiente que acompaña es la letra w, que por sí sola no tiene valor, en cambio \w tiene un significado concreto y no es más que indicar cualquier carácter alfanumérico que permite definir una palabra (w de word); no incluye espacios, tabuladores ni signos de admiración. Pero sólo indica 1 carácter, de ahí que el último símbolo sea el +, indicando que lo que le antecede en la regex puede aparecer 1 o más veces. Así que, la combinación en Java de \\w+ significa que se debe comparar con cualquier entrada que defina una palabra (sin espacios) por larga que sea. La concatenación de estos símbolos permite encontrar los términos de longitud distinta caracola y paloma.

Entidades de RegExp

Por tanto, ya puede hacerse una idea el programador de por dónde van los tiros a la hora de escribir expresiones regulares. Básicamente se trata de conocer los símbolos que puede usar y qué significa cada uno de ellos. A continuación, se describen las entidades más importantes de regex:

Símbolo Significado
( ) Los paréntesis permiten definir grupos con dos objetivos, uno para extraer la parte del input compatible y además poderle aplicar un operador de recurrencia sobre el grupo que se indique.
* + ? El primero (*) indica que lo precedente al símbolo puede aparecer 0 o n veces. El + indica que al menos debe aparecer lo anterior 1 vez, sin límite máximo. Sin embargo, ? indica que puede ocurrir 0 o 1 vez.
{n,m} {n,} {n} El primero hace referencia a que debe ocurrir entre un mínimo de n y un máximo de m veces. El segundo sólo mínimo n veces. Y la última exactamente n ocurrencias.
\w \W En minúscula hace referencia a cualquier símbolo alfanumérico considerado que puede definir una palabra. Es un alias de [a-zA-Z\_0-9]. En mayúsculas es la negación, es decir, el resto de caracteres no contemplados por \w.
[ ] Los corchetes especifican conjuntos de letras o símbolos, sin importar el orden. Incluso permite rangos con ‘-‘ como desde la a hasta la z con [a-z]. Se puede indicar la negación de un grupo de letras, si el primer carácter es ^. Por ejemplo, [^0-9] significa que no puede contener ningún dígito.
\d \D En minúscula indica dígito, sinónimo de [0-9]. En mayúsculas indica no dígito o lo que es lo mismo [^0-9].
\s \S Hace referencia a los caracteres que son espacios en blanco (\ ), tabuladores (\t) o saltos de línea (\n) y retorno de carro (\r). En mayúscula es para aquellos símbolos que no es ninguno de los anteriores.
. El punto es el comodín que incluye cualquier símbolo, pero solo 1. Se puede combinar con el _ para aceptar todas las entradas posibles: «_»
| La barra vertical indica alternación, es decir, que será compatible la entrada que cumpla el lado izquierdo o el derecho de | . Por ejemplo «hola | buenas».
^ $ Respectivamente indican inicio y fin de la secuencia de entrada. De tal manera, que una expresión que incluye éstos obligará a la entrada a coincidir por completo y no sólo un segmento. O combinaciones del estilo a que empiece o termine por… Por ejemplo «^inicio.*fin$».

Estos son los símbolos y construcciones más usados a la hora de definir expresiones regulares. Véase los siguientes ejemplos de uso, aplicados en combinación entre algunos.

La siguiente expresión permite obtener los campos de una fecha del tipo dd/MM/yy o dd/MM/yyyy. Fíjese que por cada cifra se define un grupo entre paréntesis, que permitirá obtener el valor mediante mat.group(i), siendo i = 1, 2 o 3.

// parseo de fechas
regex = "(\\d{2})/(\\d{2})/(\\d{2,4})";

En cambio, si se desea localizar el texto recogido entre las etiquetas de apertura y cierre de un fichero XML para el elemento nombre sería como sigue:

String regex = "<nombre>(.*)</nombre>";
Pattern pat = Pattern.compile(regex);
String input = "... <nombre>Pepito</nombre> ...";
Matcher mat = pat.matcher(input);
if (mat.find()) {
    System.out.println("Regexp encontrada");
    System.out.println("Sujeto:"+mat.group(1));
} else {
    System.err.println("Regexp NO encontrada");
}

Como puede verse en los ejemplos, no todos los caracteres se deben escapar como ‘<‘, ‘>’, ‘/’,…, sólo deben precederse con contra barras aquellos que tienen un significado dentro de los reconocidos por regex.

Por último, considérese el siguiente ejercicio que trata de escribir un algoritmo completo, en donde el programador pretende procesar un archivo de texto, reemplazando una directiva que permita incluir contenido dinámicamente, con el contenido de otros ficheros. La directiva podría tener el siguiente aspecto:

El siguiente fichero tiene contenido fijo,
y contenido importado con:

#include: ./include.txt

pero una frase final.

De esta manera, el programa debería ser capaz de localizar y procesar aquella expresión regular que sea capaz de aplicar la directiva \#include $<$ruta\_fichero$>$, evaluándola y sustituyendo ese fragmento por el contenido del fichero apuntado. Para simplificar el problema a resolver, se utilizarán rutas simples de ficheros en formato Unix, tal que los directorios se separan por ‘/’. La ruta de éstos podrá empezar por barra o por punto. Los del primer tipo se considerarán absolutos desde la unidad de disco actual en entornos Windows, mientras que los segundos serán relativos al directorio de trabajo del proceso Java que ejecuta el programa.

Citados los requisitos, la expresión regular que puede procesar la directiva debe contener la siguiente forma:

regex = "(#include:\\s+([\\w/.-\]+\\w+\\.txt))";

La expresión regular construye dos grupos, uno dentro de otro. El primero se utilizará para buscar la directiva completa y reemplazarla por el contenido del fichero. El segundo grupo será propiamente para recoger el fichero apuntado por ruta y nombre. Justo después del literal \#include: se indica que puede venir uno o más símbolos de espacio en blanco, de ahí \\s+. El siguiente paso es abrir el segundo de los grupos y definir un conjunto de símbolos desordenados que son, cualquier letra que forme una palabra, y caracteres ‘/’, ‘.’ o ‘-‘ una o más veces. Seguido, por último de otra palabra más la extensión .txt.

Por tanto, el bucle principal del programa que puede llevar a cabo el objetivo del enunciado es el siguiente, en donde se aprovecha la misma expresión regular para realizar varias búsquedas en el fichero, tantas como directivas \#include haya, incluso las que pueda haber dentro de otros ficheros incluidos:

// proceso principal
String regex = "(#include:\\s+(\[\\w/.-\]+\\w+\\.txt))";
Pattern pat = Pattern.compile(regex);
String input = getFileContent("input.txt");
Matcher mat = pat.matcher(input);
while (mat.find()) {
    String directive = mat.group(1);
    String filepath = mat.group(2);
    String includeContent = getFileContent(filepath);
    input = input.replace(directive, includeContent);
    mat = pat.matcher(input);
}
System.out.println(input);
// fin

// func para leer el contenido de ficheros
String getFileContent(String filepath) throws IOException {
    File file = new File(filepath);
    FileInputStream fis = new FileInputStream(file);
    byte\[\] bytes = new byte\[(int) file.length()\];
    fis.read(bytes);
    fis.close();
    return new String(bytes);
}

Para poder hacer alguna que otra prueba, el contenido inicial del primer texto debería tener al menos la siguiente forma en el fichero input.txt:

# INICIO: fichero input.txt
Esto es una prueba de contenido no variable,
con contenido importado:

#include: ./include.txt

Y finalizado con otro contenido no variable.

## FIN: fichero input.txt

Y con las siguientes líneas en el fichero include.txt para validar el correcto funcionamiento de la directiva:

\# INICIO: fichero include.txt
Este contenido es el de include.txt
FIN: fichero include.txt

La salida del proceso generará las siguientes líneas por consola:

\# INICIO: fichero input.txt
Esto es una prueba de contenido no variable,
con contenido importado:

## INICIO: fichero include.txt

Este contenido es el de include.txt
FIN: fichero include.txt

Y finalizado con otro contenido no variable.
FIN: fichero input.txt

Este es sólo uno de los ejemplos de lo que puede realizarse con las expresiones regulares. Para finalizar, indicar que el método Pattern.compile(…) acepta un segundo argumento que habilita a la expresión regular a no distinguir entre mayúsculas y minúsculas, o ingerir saltos de línea como parte de la entrada a mitad de expresión regular y otras opciones específicas por plataforma. Puede consultar más información en la página de Javadoc de la propia clase.

Tutorial Java RegExp

¿Con ganas de seguir leyendo?

Nuestra guía de Java

Cerca de 450 páginas en un libro de tapa blanda que podrás utilizar para aprender a programar en Java desde cero sin conocimientos previos. Explicamos como usar las herramientas más usadas en el mundo empresarial, todas ellas son totalmente gratis y Open Source.

Aprende conceptos como TDD para desarrollar software con garantías. Conecta tus apps con JPA en bases de datos SQL. Integra tus proyectos con Maven y mantenlos bajo control con Git. Mantente al día con la programación funcional de Java 8+.

Nuestra guía de Java
Libro Javañol