Regex o expresiones regulares: la manera más sencilla de describir secuencias de caracteres
La búsqueda de caracteres o secuencias concretas de caracteres en documentos forma parte de las tareas estándar y recurrentes de la tecnología de la información. Por lo general, el objetivo es modificar o sustituir fragmentos de texto o líneas de código, cuya complejidad aumenta en función de las veces que la secuencia de caracteres aparece en el documento. En la década de 1950 se encontró una solución basada en las lenguas formales de la informática teórica que sigue presente en el desarrollo actual de software y que permite simplificar estas tareas repetitivas mediante el uso de las denominadas expresiones regulares (en inglés, regular expressions).
¿Qué es una expresión regular?
Las regex (en inglés, regular expressions) son las unidades de descripción de los lenguajes regulares, que se incluyen en los denominados lenguajes formales. Son un instrumento clave de la informática teórica, la cual, entre otras cosas, establece las bases para el desarrollo y la ejecución de programas informáticos, así como para la construcción del compilador necesario para ello. Es por esto que las expresiones regulares, también denominadas regex y basadas en reglas sintácticas claramente definidas, se utilizan principalmente en el ámbito del desarrollo de software.
Para cada regex existe un denominado autómata finito (también conocido como máquina de estado finito) que acepta el lenguaje especificado por la expresión y que, con ayuda de la construcción de Thompson, se desarrolla a partir de una expresión regular. Por otro lado, para cada autómata finito también hay una expresión regular que describe el lenguaje aceptado por el autómata. Este puede generarse bien con el algoritmo de Kleene o bien con la eliminación de estados.
Un autómata es un modelo de conducta formado por estados, transiciones de estado y acciones. Por tanto, se considera que es finito si la cantidad de estados que puede adoptar es finita (es decir, limitada).
Un claro ejemplo del uso de regex en la tecnología de la información es la función de buscar y reemplazar de los editores de texto, la cual fue implementada por primera vez en los años 60 por el pionero en las ciencias de la computación, Ken Thompson, uno de los desarrolladores del sistema operativo UNIX, en el editor de texto por línea QED y posteriormente en sus sucesores. Esta función permite buscar determinadas secuencias de caracteres en los textos y, si se desea, reemplazarlas por otra secuencia de caracteres cualquiera.
Las regex son cadenas de caracteres basadas en reglas sintácticas que permiten describir secuencias de caracteres. Así, forman parte de los lenguajes regulares, los cuales son un subgrupo de los lenguajes formales, de gran importancia para la tecnología de la información y, especialmente, para el desarrollo de software.
¿Cómo funciona una expresión regular?
Una expresión regular puede estar formada, o bien exclusivamente por caracteres normales (como abc), o bien por una combinación de caracteres normales y metacaracteres (como ab*c). Los metacaracteres describen ciertas construcciones o disposiciones de caracteres: por ejemplo, si un carácter debe estar en el inicio de la línea o si un carácter solo debe o puede aparecer exactamente una vez, más veces o menos. Ambos ejemplos de expresiones regulares funcionan, por ejemplo, de la siguiente manera:
abc. El patrón regex sencillo abc requiere una coincidencia exacta. Por tanto, se buscarán cadenas de caracteres que no solo contengan los caracteres “abc”, sino que también aparezcan en ese orden. Una pregunta como “¿Conoces la plaza ABC?” ofrece la coincidencia buscada por esta expresión.
ab*c. Las expresiones regulares con caracteres especiales funcionan de manera diferente, ya que no solo se buscarán coincidencias exactas, si no también escenarios especiales. En este caso, el asterisco hace que la búsqueda se centre en cadenas de caracteres que empiecen por la letra “a” y que terminen por la letra “c” y entremedias cuenten con cualquier número de caracteres “b”. Así se mostrará como coincidencia tanto “abc”, como la cadena de caracteres “abbbbc” y “cbbabbcba”.
Además, cada regex se puede vincular a una acción concreta, como la ya mencionada “Reemplazar”. Esta acción se ejecuta en todos los lugares en los que se detecta la expresión regular, es decir, en todos los puntos en los que haya una coincidencia similar a la de los ejemplos.
¿Qué retos presenta el uso de las expresiones regulares?
Trabajar con instrucciones regex da margen para tomarse muchas libertades, pues siempre habrá varias soluciones posibles para cada tarea que se desee resolver con una expresión regular. El hecho de que el resultado deseado se pueda obtener de diferentes maneras, no obstante, no siempre se puede considerar una ventaja:
Para asegurar que se logrará el objetivo en todos los casos, pueden utilizarse instrucciones muy generales, pero, si se busca un resultado más exacto, entonces será inevitable formular un patrón regex específico. También vale la pena considerar la longitud: cuanto más compacta sea la expresión regular, menor será su tiempo de procesamiento. No obstante, no se debe descuidar la legibilidad. Si las instrucciones originales son demasiado complicadas y no tienen comentarios, modificarlas a posteriori es muy difícil.
Por lo general, para la creación de expresiones regulares se debe dar con la combinación perfecta entre compacidad y especificidad.
¿Qué reglas sintácticas son válidas para las expresiones regulares?
Las expresiones regulares se pueden aplicar en diversos lenguajes, tales como Perl, Python, Ruby, JavaScript, XML o HTML, por lo que los usos o funciones pueden llegar a ser muy diferentes. En JavaScript los patrones regex se utilizan, por ejemplo, en los métodos de cadena search(), match() o replace(), mientras que las expresiones en documentos XML sirven para limitar elementos de contenido. En lo que respecta a la sintaxis, entre los diferentes lenguajes de programación y lenguajes de marcado apenas hay diferencias en cuanto a las expresiones regulares:
Así, una expresión regular puede estar formada por hasta tres partes, independientemente del lenguaje en el que se va a utilizar:
Pattern (patrón de búsqueda) | El elemento central es el patrón, esto es, el patrón de búsqueda general. Tal y como hemos explicado antes, se puede formar a partir de caracteres simples o a partir de una combinación de caracteres simples y especiales. |
---|---|
Delimiter (delimitador) | El inicio y el final del patrón se identifican con delimitadores. Los delimitadores son, básicamente, todos los caracteres no alfanuméricos (excepto la barra diagonal inversa). Por ejemplo, para PHP las almohadillas (#pattern#), los signos de porcentaje (%pattern%), el signo más (+pattern+) o las tildes (~pattern~) son delimitadores. La mayoría de lenguajes ya usan las comillas (“pattern”) o las barras diagonales (/pattern/). |
Modifier (modificador) | Los modificadores pueden añadirse a un patrón de búsqueda para modificar la expresión regular. Un ejemplo es el modificador i, el cual anula la distinción entre mayúsculas y minúsculas. Garantiza que las mayúsculas y las minúsculas se tienen en consideración y que valen por defecto para todas las expresiones regulares. |
A continuación, listamos algunos de los caracteres especiales de sintaxis que pueden ampliar los patrones con opciones específicas:
Caracteres especiales regex de sintaxis | Función |
---|---|
[] | Los corchetes identifican a una clase de caracteres que siempre representa a un único carácter en un patrón de búsqueda. |
() | Los paréntesis identifican un grupo de caracteres formado por uno o varios caracteres y que pueden operarse unos dentro de los otros. |
- | Funciona a modo de especificación del área (de […] hasta […]) cuando se sitúa entre dos caracteres normales. |
^ | Limita la búsqueda al inicio de una línea (otra función: elemento de negación en clases de caracteres). |
$ | Limita la búsqueda al final de una línea. |
. | Equivale a cualquier carácter. |
* | El número del carácter, de la clase o del grupo situado antes del asterisco puede ser aleatorio (cero incluido). |
+ | El carácter, la clase o el grupo antes de un signo más debe aparecer como mínimo una vez. |
? | El carácter, la clase o el grupo antes del signo de interrogación es opcional y puede aparecer como máximo una vez. |
{n} | El carácter, la clase o el grupo anteriores aparecen exactamente n veces. |
{n,m} | El carácter, la clase o el grupo anteriores aparecen como mínimo n veces y como máximo m veces. |
{n,} | El carácter, la clase o el grupo anteriores aparecen como mínimo n veces o con frecuencia. |
\b | Tiene en cuenta el límite de palabra durante la búsqueda. |
\B | Ignora el límite de palabra durante la búsqueda. |
\d | Cualquier dígito; abreviatura para la clase de caracteres [0-9]. |
\D | Cualquier no dígito; abreviatura para la clase de caracteres [^0-9]. |
\w | Cualquier carácter alfanumérico; abreviatura para la clase de caracteres [a-zA-Z_0-9]. |
\W | Cualquier carácter no alfanumérico; abreviatura para la clase de caracteres [^\w]. |
Tutorial: las expresiones regulares explicadas con ejemplos
Una vez resumidos los conceptos básicos de regex en los apartados anteriores, pasamos ahora a explicar el modo de funcionamiento de estas prácticas cadenas de caracteres. En el tutorial a continuación, mostramos las diversas opciones y trucos sintácticos usando ejemplos de expresiones regulares, tanto en expresiones sencillas como en expresiones complejas.
Expresiones regulares de un elemento
La forma regex más sencilla es un patrón de búsqueda que tan solo prevé un único elemento como resultado. Este tipo de expresión regular de un elemento puede, por ejemplo, definirse sin problemas usando una clase de caracteres, siempre y cuando no se esté buscando un elemento concreto. La siguiente expresión permite opcionalmente los dígitos “1”, “2”, “3”, “4”, “5”, “6” o “7” como posible resultado:
[1234567]
En este caso, los números son directamente consecutivos, por lo que también se permite la siguiente grafía simplificada:
[1-7]
En caso de que la expresión regular deba modificarse para excluir de la búsqueda el dígito “4”, también se puede utilizar la variante más simple con el signo menos:
[1-35-7]
Los caracteres de un patrón regex no se separan con espacios.
Expresiones regulares de varios elementos
En el caso de las expresiones regulares de varios elementos, también se puede trabajar con clases de caracteres para permitir resultados diferentes. Si la expresión tiene que incluir, por ejemplo, dos elementos para los que se pueden esperar diferentes resultados, entonces basta con colocar dos clases de caracteres una después de la otra:
[1-7][a-c]
Como primer elemento, ha de aparecer un número de entre el “1” y el “7”, seguido de una letra “a”, “b” o “c”. Recordamos que aquí las minúsculas son obligatorias. Sin profundizar en los modificadores, con este pequeño ajuste de la expresión puedes incluir las mayúsculas:
[1-7][a-cA-C]
Expresiones regulares con elementos opcionales
Al margen de si se buscan varios elementos en una única expresión regular o con ayuda de varios grupos de caracteres, es posible que determinados elementos solo se deban o puedan incluir en determinadas condiciones. Este puede ser, por ejemplo, el caso de una expresión regular que debe filtrar todos los números de casa de las direcciones. En los casos en los que el número de casa sea un único dígito, puede coincidir con algunos resultados en los que el número está compuesto por dos o incluso tres dígitos. Además, hay direcciones en las que el número de casa incluye también una letra. Para abarcar todas estas posibles combinaciones, puedes usar las siguientes instrucciones regex:
[1-9][0-9]?[0-9]?[a-z]?
El único elemento obligatorio de este patrón de búsqueda es un número del “1” al “9”. Pueden estar seguidos opcionalmente tanto por dos dígitos del “0” al “9”, como por una letra cualquiera. Todas las opciones posibles se marcan con el signo de interrogación visto arriba.
Aunque la construcción de números de tres dígitos con letras adicionales resulta bastante clara, los números de hasta diez dígitos tienen un aspecto bastante diferente. En este caso recomendamos utilizar las llaves tal y como se muestra en la siguiente expresión regular:
[1-9][0-9]{0,9}
Al igual que en el ejemplo anterior, en primer lugar, se requiere un número del “1” al “9” que puede estar seguido de ninguno o de hasta nueve dígitos del “0” al “9”, de modo que el resultado de la búsqueda pueda estar formado por hasta diez dígitos.
Expresión regular con una cantidad aleatoria de repeticiones
En los ejemplos vistos hasta ahora para expresiones de uno o varios elementos, se conocían las cantidades mínima y máxima de caracteres. Sin embargo, hay otros escenarios en los que no se puede determinar de antemano con exactitud la cantidad de caracteres de una regex. Los parámetros necesarios son el asterisco (*) y el signo más (+), los cuales permiten una cantidad cualquiera de repeticiones de un carácter, una clase o grupos de caracteres. Se pueden registrar todas las cadenas de caracteres que tengan una cantidad cualquiera de dígitos (también “cero”), por ejemplo, con la siguiente expresión regular:
[0-9]*
Esto también es válido para la búsqueda de una combinación concreta de caracteres en la que uno (o varios) caracteres pueden aparecer con una frecuencia cualquiera, tal y como se muestra en el siguiente ejemplo:
ab*
En este caso, los resultados son las palabras “arrancar” y también “abrir”. En caso de que se deba ignorar el primer resultado o si el carácter especificado debe aparecer como mínimo una vez, entonces debe utilizarse el signo más:
ab+
Negar clases de caracteres
Si se desean utilizar expresiones regulares con clases de caracteres que equivalen a uno o varios caracteres, pero al mismo tiempo se desea descartar como resultado a uno o varios caracteres determinados, entonces se necesita utilizar la negación “^” (acento circunflejo). Este signo se sitúa siempre dentro de los corchetes de una clase de caracteres, por lo que su validez se limita a los mismos. La siguiente instrucción es un buen ejemplo de una clase de caracteres en negación:
c[^o]sa
En este caso, el segundo carácter puede ser un carácter cualquiera excepto “o”, por lo que la palabra “casa” cumple con los criterios de coincidencia. Sin embargo, la palabra “cosa” no lo cumple.
Marcador de posición
Las expresiones regulares también permiten trabajar con marcadores de posición, los cuales equivalen a uno, varios o incluso ningún carácter (dependiendo del metacarácter utilizado) dentro de un patrón de búsqueda. El marcador de posición se genera mediante un punto combinado con el carácter especial citado antes para repeticiones, siempre y cuando se desee obtener un resultado de más de un único carácter. Este tipo de expresiones regulares permite, por ejemplo, buscar en una base de datos a una persona que, aunque se conoce por su nombre y apellidos, no se tiene la certeza de si se ha introducido incluyendo un segundo nombre:
Juan.*Apellido
En este caso, los resultados pueden ser tanto “Juan Antonio Apellido” (o cualquier otra combinación de segundo nombre) como “Juan A. Apellido” o “Juan Apellido”. Si solo deben tenerse en cuenta las variantes con un segundo nombre, entonces deberá utilizarse un signo más en lugar del asterisco:
Juan.+Apellido
Un buen ejemplo de uso práctico de un marcador de posición para un único carácter es el siguiente patrón de búsqueda, en el que las coincidencias pueden ser tanto “casa” como “cosa”:
c.sa
Alternativas
Las expresiones regulares también pueden formularse de tal manera que se ofrezcan dos o más coincidencias alternativas. La alternativa tiene validez usando como separación una barra vertical, tal y como se muestra en el siguiente ejemplo:
casa|cosa
En este caso, tanto “casa” como “cosa” ofrecerán una coincidencia.
Las alternativas también pueden formularse dentro de palabras o secuencias de caracteres mediante el uso de grupos:
(Lun|Mart|Miércol|Juev|Viern)es|Sábado|Domingo
En este ejemplo, cada uno de los días de la semana pueden ser un resultado pues, gracias a la agrupación mediante paréntesis, todos los días de la semana que acaban en “es” se registran correctamente, incluso si se usan de forma abreviada.
Grupos
Los grupos de caracteres, como los del apartado anterior, son considerados clases de caracteres por los elementos estructurales de las expresiones regulares. Pueden definirse con un par de paréntesis y equivalen básicamente a un patrón compuesto por uno o varios caracteres. En el sentido estricto de la palabra, cada regex es un grupo, pero en este caso se obvia la caracterización mediante paréntesis. Dentro de las expresiones, los grupos permiten aplicar operadores tales como el delimitador o el signo de repetición (signo más o asterisco) en una expresión parcial específica:
ab(cd)+
En este caso, la repetición aleatoria que se busca también será válida para el grupo de caracteres “cd” pero, si se prescinde de los paréntesis, solo será válida para “d”. Dentro de una regex no hay límites para la cantidad de grupos incluidos.
Anidamientos
Una expresión regular no solo puede albergar una cantidad cualquiera de grupos, sino que también permite anidar tantos grupos como se desee para expresar designaciones complejas entre caracteres individuales y caracteres especiales sin necesidad de usar cadenas de caracteres excesivamente largas. Un ejemplo es el siguiente patrón regex, con el cual se pueden obtener como posible resultado los cuatro modelos de automóvil “VW Golf”, “VW Polo”, “Fiat Punto” o “Fiat Panda”:
(VW (Golf|Polo)|Fiat (Punto|Panda))
Límite de palabra
Cuando, al aplicar una expresión regular, deban tenerse en cuenta los límites de palabra, esto es, el inicio o el final de una secuencia alfanumérica, entonces se deberán especificar mediante el uso de metacaracteres. Muchos lenguajes utilizan para esto la combinación “\b”, la cual se puede anteponer, agregar o posponer al patrón de búsqueda.
La primera variante determina que la secuencia de búsqueda esté al principio de la palabra:
\bcaso
Una coincidencia para esta expresión regular es, por ejemplo, la palabra “casona”. Por el contrario, la palabra “acaso” se excluye de los resultados debido a que el carácter que se ha buscado tiene delante la letra “a”. Para el caso contrario, se puede utilizar la segunda variante y agregar el carácter especial:
caso\b
Con la tercera opción, ambos límites de palabra se convierten en requisito imprescindible; en el caso del ejemplo utilizado, el único resultado posible es la palabra “caso”:
\bcaso\b
Quitar metasignificado de caracteres especiales
En el apartado anterior, la barra diagonal inversa garantiza que la “b” colocada después no se va a utilizar como letra, sino como metacarácter. Si se combina con caracteres considerados por defecto como uno de los caracteres especiales sintácticos regex, entonces tendrá exactamente el efecto opuesto: el carácter se considerará como un literal común. Gracias a esta opción, también se puede buscar sin problemas una fecha concreta usando una expresión regular:
11\.10\.2019
En este caso, la fecha “11.10.2019” es la única cadena de caracteres que coincide con los criterios de búsqueda especificados. Si no se coloca la barra diagonal inversa, los dos puntos se considerarían marcadores de posición para un carácter cualquiera, por lo que también se ofrecerían resultados del tipo “1101092019” o “11a10b2019”.
“Moderar” expresiones regulares ambiciosas
El uso de cuantificadores (“?”, “+”, “*”, “{}”) garantiza por defecto que una expresión sea “ambiciosa” y que, por tanto, busque la mayor coincidencia posible. Como este comportamiento no siempre es deseable, los cuantificadores se pueden especificar en una expresión regular de tal manera que se modere esa “ambición”. Este proceso de modificación se muestra claramente en el siguiente ejemplo:
A.*B
Si se aplica en la secuencia de caracteres “ABCDEB”, esta expresión no para la búsqueda tras “AB”, sino que engloba como resultado toda la secuencia de caracteres. Si, por el contrario, la búsqueda se debe cancelar justo tras la primera “B”, entonces se necesita la modificación indicada. Para ello, en muchas de las lenguas (entre otras Perl, Tcl, HTML) se coloca detrás una marca de interrogación:
A.*?B
Como alternativa, la expresión original también se puede sustituir por la siguiente expresión equivalente y “no ambiciosa”, para así obtener el mismo resultado:
A[^B]*B
La limitación de expresiones regulares ambiciosas complica el procesamiento del patrón de búsqueda, por lo que está vinculado a un tiempo de búsqueda más largo.