¿Qué son las Expresiones regulares? (RegEx)

¿Sabes que es una RegEx? O mejor, ¿serías capaz de escribir una expresión para capturar números de cuenta IBAN, o validar direcciones de correo? ¿Podrías coger un fichero CSV, reordenar sus columnas, limpiar caracteres problemáticos, crear una columna nueva a partir de partes de dos otras, cambiar el carácter separador, y escapar las comillas dobles dentro de las columnas con una contrabarra, usando RegEx?

Si has contestado con un «No» a alguna de las preguntas anteriores, o simplemente, estás interesado en este tipo de expresiones, y quieres ampliar tus conocimientos, éste es tu artículo.

Carles Gaya Arasa
Carles Gaya ArasaArquitecto Cognitivo de TI en Viewnext

¿Qué es una expresión regular o RegEx?

Pero empecemos por el principio, su definición: podemos definir una Expresión Regular (o RegEx, Regular Expression), como una cadena de texto genérica, que se usa a modo de patrón, y que sirve para localizar trozos de texto dentro de otro texto mayor.

Es decir, si consideramos el patrón ‘la’ y el texto ‘la casa de la avenida es la más larga’, podrías encontrar cuatro coincidencias (match), los tres artículos ‘la’, y las dos primeras letras de la palabra ‘larga’.

O en un caso más práctico: si en Java ejecutamos “mi casa es pequeña”.split(“ “), obtendremos un array de cuatro elementos, uno con cada palabra de la frase ([“mi”, ”casa”, ”es”, ” pequeña”]. Pero lo que Java ha hecho es tratar “ “ como un patrón, lo ha buscado dentro del texto de test, y ha separado este texto según las coincidencias encontradas. Es decir, Java lo ha tratado como una RegEx, y como prueba de ello, podíamos haber indicado como patrón de separación “[^a-z0-9]”, o “\W”, o “\s”, para hacer la misma tarea, obteniendo el mismo resultado.

Como referencia histórica, solo indicar que la sintaxis fue introducida per Ken Thompson en el mundo unix en herramientas como sed, awk y grep, más tarde fue ampliada por POSIX, y posteriormente, aun mas por Perl.

Muchos procesadores de texto, sistemas operativos, o IDEs de desarrollo son capaces de entenderlas y procesarlas. Y muchos lenguajes de programación las soportan, como Java, JavaScript, Python, PHP, .NET, entre otros.

Y el hecho de entenderlas, y dominarlas, nos puede abrir una gran ventana al procesamiento del texto, para identificar patrones, procesarlos, modificarlos, etc.

¿Cómo se componen?

Para definir una RegEx, podemos usar distintos tipos de metacaracteres, dentro de una expresión. Por ejemplo, una RegEx puede contener:

  • Caracteres literales, es decir, caracteres que marcan que se debe hallar un carácter como él para hacer match (como los ejemplos iniciales, eran ‘la’, o el espacio en blanco ‘ ‘).
  • Las clases de caracteres, que representan tipologías de caracteres (y ahorrarte la faena de describirla tú), como, por ejemplo;
    • [abc]’ para designar un carácter que puede ser cualquiera de los indicados (‘a’, ‘b’, o ‘c’).
    • [0-9]cualquier carácter de la secuencia indicada (del ‘0’ al ‘9’). ‘[a-zA-Z]conjuntos de secuencias (letras minúsculas y mayúsculas).
    • [^abc]’, que es la negación de las clases indicadas (en este caso, cualquier carácter que no sea ‘a’, ‘b’, o ‘c’).
    • Hay otro metacaracter especial de uso muy común que es ’.’ (punto), que representa cualquier carácter, exceptuando saltos de línea (pero exactamente un solo carácter). Por ejemplo, el patrón ‘[aA].[^0-9]’, buscará todas las secuencias de tres caracteres que empiecen por la letra a mayúscula o minúscula, contengan cualquier carácter en medio, y acaben por un carácter no numérico.
  • Los Cuantificadores, son metacaracteres que indican como se pueden repetir los caracteres o grupos anteriores. Por ejemplo:
    • Un ? indica que el carácter anterior es opcional (se puede repetir entre 0 o 1 vez).
    • Un * indica que el carácter anterior se puede repetir entre 0 y más veces.
    • Un + indica que puede ser entre 1 y más veces.
    • Para acabar {a,b}, que indica que el carácter se puede repetir un mínimo de a veces y una máximo de b veces (con a y b numéricos opcionales). Por ejemplo, {2,} indica un mínimo de 2 y sin máximo {,3} significa un máximo de 3 y sin mínimo, o {4} indica que se debe repetir exactamente 4 veces).
    • Por ejemplo, ‘las?’ hará match con ‘la’ y con ‘las’, ya que se ha indicado que la ‘s’ es opcional. O ‘[0-9]+’ buscará secuencias de dígitos (con al menos un dígito como mínimo).
  • Hay dos Anclajes básicos, ‘^’ indica inicio de línea, y ‘$’ indica final de línea. Sirven, para asegurar que la coincidencia es de la línea entera. Estos anclajes no consumen caracteres (zero-length anchor). Por ejemplo ‘^a’, contra el texto ‘a b a c’ solo haría match con la primera ‘a’. Pero el patrón ‘^’, es un anclaje al principio de cada línea, pero no devuelve ningún carácter. Ver más adelante, anclajes de PERL, \b, por ejemplo.
  • Las Agrupaciones sirven para agrupar conjuntos de caracteres, para que, por ejemplo, les aplique un mismo metacaracter. Por ejemplo, en ‘(ma)+b’ haría coincidencia con ‘mab’ y con ‘mamab’, pero no con ‘maab’. Se indican agrupando el conjunto con paréntesis ‘()’. Además, también permiten:
    • Las agrupaciones pueden ser referenciadas posteriormente, con ‘\ipara indicar el número de agrupación (backreference). Por ejemplo, ‘([abc]) y \1’, haría match con ‘a y a’, pero no con ‘a y b’, ya que ‘\1’ indica el valor capturado en el primer grupo (‘a’ en el ejemplo).
    • También se pueden indicar referencias a los grupos capturados, en procesadores de texto que permitan reemplazar texto (Find&Replace). Para indicar en el texto de sustitución, el valor de un grupo capturado (\1-\9 del primer al noveno grupo, y \0 para todo el texto coincidente).
  • Se pueden indicar Alternativas, como si fuera una OR lógica, con ‘|’. Por ejemplo, ‘niño|a’, trata la ‘a’ y la ‘o’ adyacentes al pipe como alternativas, y haría match con ‘niña’ y con ‘niño’. Se permite tratar agrupaciones como alternativas, por ejemplo, ‘(hombre)|(mujer)’ haría coincidencia con ‘hombre’ y con ‘mujer’.
  • En caso que se quiera indicar un carácter que sí se quiere que se use con el valor formal de este, y no como un metacaracter, éste debe ser escapado con una contrabarra ’\’. Por ejemplo, “\.\?” hace coincidencia con el texto ‘.?’, pero no significa un carácter cualquiera opcional (que es lo que significaría la RegEx ‘.?’)

POSIX hizo varias extensiones, a las RegEx, entre ellas:

  • Definió maneras de identificar clases de caracteres con la nomenclatura [:nombre:], donde nombre indica el nombre de la clase. Por ejemplo, se definió [:digit:] para identificar la clase de todos los dígitos ([0-9]), [:alpha:] identifica los caracteres alfabéticos ([a-zA-Z]), [:blank:] corresponde a un espacio o un tabulador, y, entre otros, podemos nombrar [:upper:], [:lower:], [:punct:], [:print:], o [:alnum:].
  • POSIX también definió la capacidad voraz de los repetidores (lazy/greedy). Por defecto, la RegEx intentará coger el máximo número de caracteres, para hacer match (funcionamiento voraz o greedy). Por ejemplo, la regex ‘perr.*o’, en el texto ‘el perro busca un hueso gris’, encontrará la coincidencia ‘perro busca un hueso’ (mapeando ‘.*’ con ‘o busca un hues’). Si nosotros queremos que la coincidencia sea con el mínimo número de caracteres, debemos indicar el comportamiento perezoso o ‘lazy’, indicando un ‘?’ después del repetidor. Por ejemplo, la regex ‘perr.*?o’, en el texto ‘el perro busca un hueso gris’, encontrará únicamente la coincidencia ‘perro’ (mapeando ‘.*?’ con ‘’ (vacío)).

PERL también hizo algunas mejoras a RegEx:

  • Se definieron otra manera de identificar clases de caracteres, y grupos de caracteres. Por ejemplo, \d indica un dígito (como [0-9]), \D indica un carácter no dígito ([^0-9]), \w indica carácter de una ‘palabra extendida’ ([a-zA-Z0-9_]), y \W el contrario [^a-zA-Z0-9_], entre otros.
  • También definió nuevos anclajes, como el de límite de palabra \b, que se podría definir como un cambio entre un carácter ‘espacio’ a un carácter ‘no espacio’, o al revés (anclaje que no consume caracteres).
  • También definió modificadores de la búsqueda, como i (case insensitive), m (multilínea), o g (global, búsqueda múltiple), entre otros.
  • Otra adición de PERL son las Aserciones (LookAhead y LookBehind, positive y negative). Estas consisten en condiciones que se deben cumplir, hacia delante o hacia atrás del punto en que se indican. No consumen caracteres (en tanto que solo aseguran que se cumpla cierta restricción). Por ejemplo, el mira-adelante positivo LookAhead, se indica como ‘(?=pattern)‘ (con pattern el patrón a buscar hacia adelante). En el caso ’^(?=.*coche)(?=.*moto)’ se está buscando una línea entera que contenga las palabras ‘coche’ y ‘moto’, en cualquier orden, y se posiciona al principio de la línea sin consumir ningún carácter. El mira-adelante negativo NegativeLookAhead, sirve para validar un patrón que no debe existir, y se indica como ‘(?!pattern)‘ (con pattern el patrón a validar que no existe). También existen LookBehind positivos y negativos, que sirven para lo mismo, es decir, validar un patrón a existir (o no existir), pero busca el patrón a la izquierda de la posición de evaluación, hacia atrás. Se indican como ‘(?<=pattern)‘ para LookBehind, y ‘(?<!pattern)‘ para NegativeLookBehind.
  • PERL también define Condicionales, amplia las backreference, y Agrupaciones sin captura de texto, entre otros.

Primeros pasos

Lo primero que debemos saber, antes de empezar a definir patrones Regex es que habitualmente, hay más de una manera de definir un mismo comportamiento. Y estos patrones se pueden ajustar mejor o peor a lo que queremos capturar, o pueden ser más o menos eficientes.

Otra lección aprendida consiste en usar un validador online de RegEx (ver ejemplos más abajo), poner unos cuantas muestras de lo que queremos capturar, y también de lo que no queremos capturar, y empezar a escribir el patrón poco a poco.

Empezando por las partes más sencillas, e ir complicándolo poco a poco. Cuando lleguemos a un patrón que no funcione, ir simplificándolo hasta que vuelva a funcionar, e identificar los cambios hechos, y su repercusión.

Ejemplo práctico: Cómo capturar un tag XML

Para capturar un tag xml, podríamos escribir algo como ‘<[a-z]+>’, pero eso nos capturará tags alfabéticos, de apertura o de clausura, pero no soporta tags con números, ni con atributos. Podríamos pasar a ‘<[^><]+>’, para ser más tolerantes, aunque ahí habría problemas aun, con contenido CDATA con tags.

Podríamos ir más lejos, y validar que exista un tag de apertura y otro de clausura apareados (no funcionaría con los tags externos en tags anidados), o que sea un tag autocerrado, con el patrón ‘(?:<([^>< ]+)([^><]*)>[^<]*<\/\1>)|(?:<([^>< ]+)([^><]*)\/>)’.

Este consistirá en: grupos sin captura de datos con dos alternativas, la primera consiste en ‘<’, seguido de un carácter o más, no ‘<’, ‘>’, ni blanco, seguido de caracteres opcionales no ‘<’, ‘>’ (donde irían los atributos del tag).

Luego  un ‘>’, seguido del contenido del tag (caracteres opcionales no ‘<’), al lado de ‘</’, seguido del valor del primer grupo (tag capturado), por último un ‘>’. La segunda alternativa es más simple, porque no contiene el texto en innerTag ni el tag de cierre.

Otro caso de uso

A nivel de coincidencia es correcto pero a nivel de rendimiento es muy diferente. El patrón ‘(?=.*moto)(?=.*coche)’ para buscar si una frase contiene las palabras ‘moto’ y ‘coche’, nos haría match en la frase ‘yo tengo un coche que chocó con la moto de mi vecino’. El problema es que concretamente haría match 13 veces (en cada posición entre el inicio de la frase y la palabra coche). Si cambiamos el patrón a ‘^(?=.*moto)(?=.*coche)’, sigue haciendo match, pero solo da una coincidencia, ya que el patrón solo se evalúa una vez (al principio de la frase, ya que el patrón empieza con el anclaje ‘^’), mejorando el rendimiento en una razón de 30 a 1.

Los RegEx más utilizados

Basta buscar en cualquier buscador web ‘regex cualquier cosa’, para encontrar numerosos patrones para validar cualquier cosa.

A modo de ejemplo, se añaden algunos:

  • Validador de DNIs: ‘\b\d{8}[-| ]?[trwagmyfpdxbnjzsqvhlcke]?\b’
  • Validador de emails: ‘^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$’
  • Validador de tarjetas: ‘^([0-9]{4} ?){3}([0-9]{1,4})$’
  • Validador de IPs v4: ‘^(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)(?:\.(?!$)|$)){4}$’
  • Validador de contraseñas (8 caracteres no blancos mínimos, y un carácter de puntuación, una minúscula y una mayúscula, como mínimo): ‘^(?=^\S{8,}$)(?=.*[!@#%?&*!\\|]{1,})(?=.*[a-z]{1,})(?=.*[A-Z]{1,}).*$’
  • Extractor de url en anchor HTML: ‘<a\shref=(.*?)<\/a>’
  • Líneas comentadas en ficheros java: ‘//[^\r\n]*[\r\n]’

Es importante entender los patrones, ya que muchas veces, queremos hacer algo parecido al patrón X que hemos encontrado en internet, pero con algún matiz. Para hacer esos cambios, y que el patrón siga funcionando bien, debemos entender como éste está funcionando. Por ejemplo, para añadir/quitar alguna restricción al validador de contraseñas indicado, debemos entender que hace, y como procesa el texto, para realizar nuestra modificación.

Recursos 

Hay numerosas páginas web con explicaciones de Regex. Por ejemplo, en Wikipedia https://es.wikipedia.org/wiki/Expresi%C3%B3n_regular 

Y muchos ejemplos, en: 

También hay numerosos validadores online, que además de permitirte hacer pruebas, te descomponen los patrones en sus componentes internos usados, ayudas en componentes disponibles, así como te dan datos de rendimiento (procesado, ejecución), ejemplos, te permiten debugar, o generador código: 

Otros artículos relacionados

2023-09-06T10:56:39+01:0026 octubre, 2022|

¡Compártelo en tus redes sociales!

Ir a Arriba