Guía de expresiones regulares en Java

En esta guía, veremos cómo usar expresiones regulares en Java, así como ejemplos prácticos de tareas y patrones comunes.

Introducción

Las expresiones regulares (RegEx) son una de las herramientas más poderosas en la programación, pero también son comúnmente malinterpretadas. Te ayudan a unir patrones de forma flexible, dinámica y eficiente, además de permitirte realizar operaciones en función de los resultados.

Esto puede incluir validar ciertos patrones que existen en un texto determinado, encontrar estas coincidencias, extraerlas y reemplazarlas, etc. Por ejemplo, ¿alguna vez intentó registrarse en un sitio web y descubrió que rechazaron su contraseña por no incluir números o ¿letras mayúsculas? Existe una gran posibilidad de que este sitio web haya utilizado expresiones regulares para asegurarse de poner los caracteres correctos.

En esta guía, profundizaremos en las expresiones regulares, cómo funcionan y cómo usarlas en Java. Principalmente vamos a echar un vistazo a las clases Pattern y Matcher del paquete regex, seguidas de algunos ejemplos prácticos y tareas comunes.

If you'd like to read more about the built-in support for Regular Expressions with Java Strings - read our Java: guía para la compatibilidad con cadenas RegEx integradas!

¿Qué son las expresiones regulares?

Las expresiones regulares (RegEx) son patrones que se utilizan para hacer coincidir caracteres en algún texto. Estos patrones se denominan patrones de búsqueda y nos permiten encontrar un patrón determinado en una determinada cadena o conjuntos de cadenas. Podemos validar la presencia de este patrón, contar sus instancias y luego extraerlo o reemplazarlo fácilmente, cuando lo encontramos.

Clases de expresiones regulares de Java

La API estándar de Java nos proporciona varias clases para trabajar con expresiones regulares, directamente desde el primer momento:

  1. Interfaz MatchResult
  2. Clase Coincidencia
  3. Clase Patrón
  4. Excepción de sintaxis de patrón

Todos estos encajan perfectamente en el paquete java.util.regex, que se puede importar fácilmente como:

1
2
// Importing all of the classes/interfaces from the regex package
import java.util.regex.*;
1
2
3
4
// You can alternatively import certain classes individually
// To reduce overhead
import java.util.regex.Pattern;
import java.util.regex.Matcher;

La clase Patrón

Una instancia de Pattern es la representación compilada de una expresión regular determinada. El Patrón no tiene constructores públicos, sino que utiliza el método .compile() para crear y devolver una instancia de Patrón.

El método .compile() toma algunos parámetros, pero se utilizan principalmente dos. El primer argumento es la Expresión regular en formato de cadena y el segundo es el marcador de coincidencia. El indicador de coincidencia se puede configurar para incluir CASE_INSENSITIVE, LITERAL, MULTILINE o varias otras opciones.

Vamos a crear una instancia de Pattern con una expresión regular representada por una cadena:

1
2
Pattern p = Pattern.compile("Stack|Abuse"); 
System.out.println(p);

Esto genera lo siguiente:

1
Stack|Abuse

Esta no es una salida demasiado sorprendente: es más o menos la misma que la cadena que pasamos al constructor Pattern. Sin embargo, la clase en sí no nos ayudará mucho por sí sola: tenemos que usar un ‘Matcher’ para hacer coincidir el RegEx compilado con alguna cadena.

La instancia Matcher para un Pattern se puede crear fácilmente a través del método matcher() de la instancia Pattern:

1
2
Pattern p = Pattern.compile("Stack|Abuse", Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher("If you keep calling the method many times, you'll perform abuse on the stack.");

Este Matcher se puede usar para poner en uso el patrón compilado.

La clase Matcher

La clase Matcher tiene varios métodos que nos permiten poner en uso un patrón compilado:


Método Descripción Devoluciones .matches() Comprueba si Regex coincide con la entrada dada. booleano .group() Extrae la subsecuencia coincidente. Cuerda .start() Obtiene el índice inicial de la subsecuencia coincidente. En t .end() Obtiene el índice final de la subsecuencia coincidente. En t .find() Encuentra la siguiente expresión disponible que coincida con el patrón Regex. booleano .find(int start) Encuentra la siguiente expresión disponible que coincida con el patrón Regex comenzando en un índice dado. booleano .groupCount() Encuentra el número total de coincidencias. En t


Con estos, puede ser bastante creativo en términos de lógica: encontrar los índices iniciales de las secuencias, el número total de coincidencias, las secuencias mismas e incluso extraerlas y devolverlas. Sin embargo, estos métodos pueden no ser tan intuitivos como parecen.

{.icon aria-hidden=“true”}

Nota: Tenga en cuenta que matches() comprueba toda la cadena, no una sección determinada. find() itera a través de la cadena y devuelve verdadero en cada aparición.

Por lo general, el método find() se utiliza con un bucle while():

1
2
3
4
while (m.find()) {
    System.out.println(String.format("Matched sequence: %s", m.group()));
    System.out.println(String.format("Start and end of sequence: %s %s \n", m.start(), m.end()));
}

Esto resulta en:

1
2
3
4
5
Matched sequence: abuse
Start and end of sequence: 58 63

Matched sequence: stack
Start and end of sequence: 71 76

Además, cada grupo es un valor delimitado por paréntesis dentro del Patrón. En nuestro caso, no hay grupo ya que no hay paréntesis que abarquen Stack|Abuse. La llamada groupCount() siempre devolverá 0 en nuestro Pattern. El método group() también depende de esta distinción, e incluso puede obtener grupos dados pasando sus índices en el patrón compilado.

Convirtamos este RegEx en dos grupos:

1
2
3
4
5
6
7
8
9
Pattern p = Pattern.compile("(Stack)|(Abuse)", Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher("If you keep calling the method many times, you'll perform abuse on the stack.");

System.out.println("Number of groups: " + m.groupCount());

while (m.find()) {
    System.out.println(String.format("Matched sequence: %s", m.group()));
    System.out.println(String.format("Start and end of sequence: %s %s\n", m.start(), m.end()));
}
1
2
3
4
5
6
Number of groups: 2
Matched sequence: abuse
Start and end of sequence: 58 63

Matched sequence: stack
Start and end of sequence: 71 76

El método group() te permite extraer grupos, incluso en función de sus índices o nombres, de una cadena dada, después de que se haya emparejado. Pero tenga cuidado con la iteración, no sea que termine encontrándose con coincidencias nulas o IllegalStateExceptions.

Una vez que comienzas a iterar a través de un patrón, se cambia globalmente.

Por lo tanto, si desea obtener diferentes grupos, como por ejemplo, extraer grupos en representaciones de fecha y hora de cadena o el host de una dirección de correo electrónico, debe iterar a través de la cadena a través de find() y obtener el siguiente grupo disponible a través de m.group() o ejecuta matches() y obtén los grupos manualmente:

1
2
3
4
5
6
7
Pattern p = Pattern.compile("(Stack)(Abuse)", Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher("wikihtp");

System.out.println("Number of groups: " + m.groupCount());
if(m.matches()) {
    System.out.println(String.format("Group 1: '%s' \nGroup 2: '%s'", m.group(1), m.group(2)));
}
1
2
3
Number of groups: 2
Group 1: 'Stack' 
Group 2: 'Abuse'

La clase matches() solo devolverá true si la secuencia completa coincide con RegEx y, en nuestro caso, esta es la única entrada para la que se activará.

Más sobre grupos en una sección posterior.

Anatomía de las expresiones regulares

Una vez familiarizado con las clases que usa Java para representar expresiones regulares y las clases que usa para hacer coincidir las secuencias en cadenas, entremos en las expresiones regulares.

Las expresiones regulares no solo consisten en cadenas literales, como las hemos usado hasta ahora. Se componen de metacaracteres, cuantificadores, caracteres de escape y grupos. Echemos un vistazo a estos individualmente.

Metacaracteres

Metacaracteres, como su nombre lo indica, brindan metainformación sobre RegEx y nos permiten crear expresiones dinámicas, en lugar de expresiones estáticas literales. Un metacarácter tiene un significado especial dentro de una expresión regular y no coincidirá como una cadena literal, y se usan como comodines o sustitutos para varios patrones de secuencias.

Algunos de los metacaracteres más utilizados son:


Significado del metacarácter . Encuentra una coincidencia de un personaje ^ Encuentra una coincidencia al principio de una cadena $ Encuentra una coincidencia al final de una cadena \d Encuentra un dígito \D Encuentra un número que no sea un dígito \s Encuentra un carácter de espacio en blanco \S Encuentra un carácter que no sea un espacio en blanco \w Encuentra un carácter de palabra [a-zA-Z_0-9] \W Encuentra un carácter que no sea una palabra \b Encuentra una coincidencia delimitada por una palabra \B Encuentra una coincidencia de límite que no sea de palabra


Puede usar cualquier cantidad de estos metacaracteres, aunque para expresiones más largas, pueden complicarse un poco.

Por ejemplo, cambiemos nuestro patrón de expresión regular anterior por uno que busca una secuencia que comienza con una letra mayúscula, contiene una secuencia de 4 letras después de eso y termina con "Stack":

1
2
3
4
5
6
7
Pattern p = Pattern.compile("^(H)(....)(Stack)$", Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher("HelloStack");

while (m.find()) {
    System.out.println(String.format("Matched sequence: %s", m.group()));
    System.out.println(String.format("Start and end of sequence: %s %s\n", m.start(), m.end()));
}
1
2
Matched sequence: HelloStack
Start and end of sequence: 0 10

Sin embargo, usar solo metacaracteres nos limita hasta cierto punto. ¿Qué pasaría si quisiéramos verificar cualquier secuencia de caracteres, en lugar de 4?

Cuantificadores

Los cuantificadores son un conjunto de caracteres que nos permiten definir cantidades de metacaracteres que coinciden


Significado del cuantificador n+ Encuentra una coincidencia de al menos uno o más de n n* Encuentra una coincidencia de 0 o más de n ¿norte? Encuentra una coincidencia de 1 o ninguna de n n{x} Encuentra una coincidencia que contenga la secuencia de n para x veces n{x, y} Encuentra una coincidencia que contenga la secuencia de n entre x e y veces n{x,} Encuentra una coincidencia que contenga la secuencia de n por al menos x veces


Por lo tanto, podríamos modificar fácilmente nuestro RegEx anterior con estos. Por ejemplo, intentemos hacer coincidir una cadena dentro de otra cadena que comienza con "Hola", seguida de cualquier secuencia de caracteres y termina con tres signos de exclamación:

1
2
3
4
5
6
7
Pattern p = Pattern.compile("(Hello)(.*)(!{3})$", Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher("I wake up and think go myself: Hello Wonderful World!!!");

while (m.find()) {
    System.out.println(String.format("Matched sequence: %s", m.group()));
    System.out.println(String.format("Start and end of sequence: %s %s\n", m.start(), m.end()));
}

Esto resulta en:

1
2
Matched sequence: Hello Wonderful World!!!
Start and end of sequence: 31 55

Caracteres de escape {#caracteres de escape}

Si desea escapar de los efectos de cualquier carácter especial, como un metacarácter o un cuantificador, puede escapar de ellos prefijándolos con un \. Sin embargo, dado que estamos definiendo un RegEx dentro de una cadena, también deberá escapar del carácter de escape. Por ejemplo, si desea hacer coincidir un signo de dólar, lo que normalmente significaría hacer coincidir si una secuencia determinada se encuentra al final de una cadena, escaparía de sus efectos y escaparía del carácter de escape en sí:

1
2
3
4
5
Pattern p = Pattern.compile("$", Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher("It costs $2.50");

Pattern p2 = Pattern.compile("\\$", Pattern.CASE_INSENSITIVE);
Matcher m2 = p.matcher("It costs $2.50");

El primer comparador coincide si la cadena termina con la secuencia que antecede al carácter $, que en este caso está en blanco. Esto es “verdadero”, ya que la cadena termina con, bueno, nada: el patrón se encontraría al final, en el índice 14. En el primer comparador, buscamos el signo de dólar real, que coincide con la cadena en el índice correcto en nuestra entrada.

Ninguno de estos dos fragmentos de código daría como resultado una excepción, así que tenga cuidado de verificar si sus expresiones regulares fallan silenciosamente, como en el primer caso.

Grupos

Hemos usado grupos un poco hasta ahora; nos permiten encontrar coincidencias para múltiples conjuntos. Puede agrupar cualquier número de conjuntos juntos o como conjuntos separados. A menudo, los grupos se utilizan para permitirle segregar algunas entradas en secciones conocidas y luego extraerlas, como diseccionar una dirección de correo electrónico en nombre, símbolo y host.

Grupo 0 denota el patrón completo, mientras que todos los demás grupos se nombran como Grupo 1, Grupo 2, Grupo n...

1
Pattern  (A)(B)(C) 

Grupo 0 denota el patrón completo, Grupo 1 es A, Grupo 2 es B y Grupo 3 es C.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
String email = "[correo electrónico protegido]";

// The entire expresion is group 0 -> Trying to match an email value
// The first group is trying to match any character sequence
// The second group is trying to match the @ symbol
// The third group is trying to match the host name as any sequence of characters
// The final group is trying to check whether the organization type consists of 3 a-z characters
String email = "[correo electrónico protegido]";

Pattern pattern = Pattern.compile("(.*)(@)(.*)(.[a-z]{3})");
Matcher matcher = pattern.matcher(email);

if (matcher.find()) {
    System.out.println("Full email: " + matcher.group(0));
    System.out.println("Username: " + matcher.group(1));
    System.out.println("Hosting Service: " + matcher.group(3));
    System.out.println("TLD: " + matcher.group(4));
}

{.icon aria-hidden=“true”}

Nota: \w denota una palabra y es una forma abreviada de [a-zA-Z_0-9]. Cualquier palabra que contenga cualquier combinación de caracteres en minúsculas y/o mayúsculas, así como números.

Este código da como resultado:

1
2
3
4
Full email: [correo electrónico protegido]
Username: someone
Hosting Service: gmail
TLD: com

Usos de expresiones regulares y ejemplos de Java

Algunos de los casos de uso más comunes de expresiones regulares son validación, búsqueda y extracción y reemplazo. En esta sección, usemos las reglas que hemos presentado hasta ahora para validar, buscar y extraer, así como reemplazar ciertos patrones de texto. Después de estas tareas, realizaremos algunas tareas comunes, como hacer coincidir dígitos, caracteres únicos o múltiples, etc.

Validar cadena en Java con expresiones regulares

Puede validar si un determinado patrón está presente en el texto, que puede ser tan simple como una sola palabra, o una de las diversas combinaciones que puede producir con diferentes metacaracteres, caracteres y cuantificadores. Un ejemplo simple podría ser encontrar si una palabra está presente en algún texto:

En esta parte, comprobaremos si un determinado patrón, en este caso solo una palabra, está en un texto. Por supuesto, aún puede validar que existe cierto patrón en un texto. Vamos a buscar la palabra "validar" en un texto de ejemplo.

1
2
3
4
5
6
7
Pattern pattern = Pattern.compile("validate");
String longText = "Some sort of long text that we're looking for something in. " +
 "We want to validate that what we're looking for is here!";

Matcher matcher = pattern.matcher(longText);
boolean found = matcher.find();
System.out.println(found); 

Esto resulta en:

1
true

Un ejemplo más realista sería validar una dirección de correo electrónico, para verificar si alguien realmente ingresó una dirección válida o simplemente usó algún valor de correo no deseado. Un correo electrónico válido contiene una secuencia de caracteres, seguida de un símbolo @, un nombre de host (otra secuencia de caracteres) y un indicador de organización, que contiene tres letras y puede ser cualquier combinación: edu, com, org , etc.

Sabiendo esto, para validar una dirección de correo electrónico usando RegEx en Java, compilaremos la expresión y usaremos el método matches() para comprobar si es válida:

1
2
3
4
5
Pattern pattern = Pattern.compile("\\w*[@]\\w*[.][a-z]{3}");

Matcher matcher = pattern.matcher("[correo electrónico protegido]");
boolean match = matcher.matches();
System.out.println(match);

Esto resulta en:

1
true

Buscar y extraer patrones en Java con expresiones regulares

A menudo, además de solo la validación, desea encontrar los puntos de inicio y finalización de una secuencia determinada. Con esto, puede crear funciones Buscar de alto rendimiento para aplicaciones de edición de texto, automatizando el proceso de búsqueda. Además, puede acortar la búsqueda de palabras clave en una página, una carta de solicitante o cualquier tipo de texto encontrando las secuencias que le interesan y, por ejemplo, resaltándolas para un operador humano.

Para encontrar el inicio y el final de una secuencia usando Expresiones Regulares, como hemos visto antes, podemos usar los métodos start() y end() de la instancia Matcher:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Pattern pattern = Pattern.compile("(search|match)");

String searchText = "You can easily search for a keyword in text using RegEx. " +
                "A keyword is just a sequence of characters, that are easy to match.";

Matcher matcher = pattern.matcher(searchText);

while (matcher.find()) {
    System.out.println("Found keyword: " + matcher.group());
    System.out.println("Start index is: " + matcher.start());
    System.out.println("End index is: " + matcher.end() + "\n");
}

La salida será la siguiente:

1
2
3
4
5
6
7
Found keyword: search
Start index is: 15
End index is: 21

Found keyword: match
Start index is: 118
End index is: 123

Aquí, también hemos extraído las palabras clave: puede registrarlas con fines analíticos, enviarlas a un terminal, como este, o manipularlas o actuar sobre ellas. Puede tratar ciertas palabras clave en el texto como puertas de enlace para ejecutar otros métodos o comandos.

Por ejemplo, al crear salas de chat u otras aplicaciones donde un usuario puede comunicarse con otros usuarios, ciertas palabras pueden censurarse para conservar una experiencia positiva. En otros casos, ciertas palabras pueden generar una señal de alerta para los operadores humanos, donde puede parecer que un usuario determinado está incitando a un comportamiento que no debería ser incitado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Pattern pattern = Pattern.compile("(fudge|attack)");

String message = "We're launching an attack at the pudding palace." +
                "Make way through all the fudge, the King lies beyond the chocolate!";

Matcher matcher = pattern.matcher(message);

while (matcher.find()) {
    System.out.println("Found keyword: " + matcher.group());
    System.out.println("Start index is: " + matcher.start());
    System.out.println("End index is: " + matcher.end());
            
    if(matcher.group().equals("fudge")) {
        System.out.println("This word might be inappropriate!");
    } else if(matcher.group().equals("attack")) {
        System.out.println("911? There's an attack going on!");
    }
}

Sin embargo, las cosas pueden no ser tan sombrías como imaginas:

1
2
3
4
5
6
7
8
9
Found keyword: attack
Start index is: 19
End index is: 25
911? There's an attack going on!

Found keyword: fudge
Start index is: 73
End index is: 78
This word might be inappropriate!

La censura no mola.

Extracción de direcciones de correo electrónico del texto

¿Qué sucede si acaba de recibir un montón de texto que contiene direcciones de correo electrónico y desea extraerlas, si son direcciones válidas? Esto no es raro cuando se extraen páginas web para, por ejemplo, información de contacto.

{.icon aria-hidden=“true”}

Nota: El web scraping, cuando se realiza, debe hacerse de manera ética, y solo si el archivo robot.txt de un sitio web lo permite. Asegúrese de cumplir con los ToS y de no enviar spam al tráfico y las conexiones de un sitio web, lo que podría causar daños a otros usuarios y a los propietarios del sitio web.

Avancemos y analicemos un poco de texto "raspado" para extraer direcciones de correo electrónico de él:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Pattern pattern = Pattern.compile("\\w*[@]\\w*[.][a-z]{3}");
String text = "We want to extract all email in this text. " +
                "Yadda yadda, some more text." +
                "[correo electrónico protegido]\n" +
                "[correo electrónico protegido]\n" +
                "[correo electrónico protegido]\n";
Matcher matcher = pattern.matcher(text);

List<String> emailList = new ArrayList<>();
while(matcher.find()) {
    emailList.add(matcher.group());
}

System.out.println(emailList);

El resultado serán todos los correos electrónicos encontrados en el texto:

1
[april@treutel.com, arvid@larkin.net, wrowe@quigley.org]ß

Coincidencia de caracteres individuales

Para hacer coincidir un solo carácter, como hemos visto antes, simplemente lo denotamos como .:

1
2
3
4
Pattern pattern = Pattern.compile(".tack");
Matcher matcher = pattern.matcher("Stack");
boolean match = matcher.matches();
System.out.println(match);

Esto resulta en:

1
true

Coincidencia de varios caracteres

La coincidencia de varios caracteres se puede reducir a un . cuantificado, pero es mucho más común: en su lugar, usará un rango de caracteres. Por ejemplo, vamos a comprobar si una cadena dada tiene algún número de caracteres, pertenecientes al rango del alfabeto:

1
2
3
4
5
6
7
8
9
Pattern pattern = Pattern.compile("[a-z]+");
Matcher matcher = pattern.matcher("stack");
boolean match = matcher.matches();
System.out.println(match);

Pattern pattern2 = Pattern.compile("[a-z]+");
Matcher matcher2 = pattern2.matcher("stack99");
boolean match2 = matcher2.matches();
System.out.println(match2);

Esto resulta en:

1
2
true
false

La segunda verificación devuelve falso ya que la cadena de entrada no solo contiene los caracteres que pertenecen al alfabeto en minúsculas, sino también números.

Coincidencia de secuencias de palabras

En lugar de rangos alfabéticos, también puede hacer coincidir patrones de \w, que es una abreviatura de [a-zA-Z_0-9]:

1
2
3
4
5
6
7
8
9
Pattern pattern = Pattern.compile("\\w*");
Matcher matcher = pattern.matcher("stack");
boolean match = matcher.matches();
System.out.println(match);

Pattern pattern2 = Pattern.compile("\\w*");
Matcher matcher2 = pattern2.matcher("stack!");
boolean match2 = matcher2.matches();
System.out.println(match2);

Esto resulta en:

1
2
true
false

Coincidencia de secuencias sin palabras

Similar a \w, \W es otra forma abreviada. Es una versión abreviada de secuencias que no son palabras. Es esencialmente un reverso de \w, excluyendo todos los caracteres que caen en la categoría de [a-zA-Z_0-9]:

1
2
3
4
5
6
7
8
9
Pattern pattern = Pattern.compile("\\W*");
Matcher matcher = pattern.matcher("stack");
boolean match = matcher.matches();
System.out.println(match);

Pattern pattern2 = Pattern.compile("\\W*");
Matcher matcher2 = pattern2.matcher("?????");
boolean match2 = matcher2.matches();
System.out.println(match2);

Esto resulta en:

1
2
false
true

? no está en el rango [a-zA-Z_0-9], por lo que el segundo comparador devuelve falso.

Coincidencia de dígitos y no dígitos

Verificando si un dígito está presente, podemos usar \d, y verificar cualquier número de dígitos es tan fácil como aplicarle un comodín. Siguiendo la misma convención que antes, \D denota no dígitos en lugar de dígitos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Pattern pattern = Pattern.compile("\\d*"); 
Matcher matcher = pattern.matcher("999");
boolean match = matcher.matches();
   
Pattern pattern2 = Pattern.compile("\\D*");
Matcher matcher2 = pattern2.matcher("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
boolean match2 = matcher2.matches();
   
System.out.println(match);
System.out.println(match2);

La salida será la siguiente:

1
2
true
true

Conclusión

Las expresiones regulares (RegEx) son una de las herramientas más poderosas en la programación, pero también son comúnmente malinterpretadas. Te ayudan a unir patrones de forma flexible, dinámica y eficiente, además de permitirte realizar operaciones en función de los resultados.

Pueden ser abrumadores, ya que las secuencias complejas tienden a volverse muy ilegibles; sin embargo, siguen siendo una de las herramientas más útiles en la actualidad. En esta guía, hemos repasado los conceptos básicos de las expresiones regulares y cómo usar el paquete regex para realizar la coincidencia de patrones en Java.