Expresiones regulares, también conocidas como regex

Este capítulo explica cómo usar expresiones regulares para procesar texto.

Las regex, o expresiones regulares, ofrecen una forma tanto de buscar como de modificar texto. Sus ventajas son que son concisas, se ejecutan muy rápido, pueden trasladarse entre lenguajes (¡definitivamente no son exclusivas de Python!) y son muy potentes. La desventaja es que resultan confusas y cuesta un poco acostumbrarse a ellas.

Puedes probar regex en vivo en un par de lugares; el primero es el propio Visual Studio Code. Para ello, haz clic en la lupa del panel de opciones de la izquierda. Cuando aparezca la barra de búsqueda, puedes escribir un término de búsqueda. A la derecha del cuadro de texto hay tres botones, uno de los cuales es un punto seguido de un asterisco. Esta opción permite que la función de búsqueda de texto de Visual Studio acepte expresiones regulares. Esto aplicará la regex a todo el texto de tu espacio de trabajo actual de Visual Studio.

Otra opción es ir a https://regex101.com/ o https://regexr.com/ y empezar a escribir tu expresión regular allí (también vale mucho la pena consultar las hojas de referencia y los patrones de referencia de regexr). Tendrás que añadir algo de texto en el cuadro para que se le aplique la regex.

Prueba cualquiera de las opciones anteriores con la regex string \w+\s. Esta encuentra cualquier aparición de la palabra ‘string’ seguida de otra palabra y luego un espacio en blanco. Por ejemplo, ‘string cleaning’ se detectaría como coincidencia al usar esta regex.

En Python, la librería re ofrece soporte para expresiones regulares. Probémosla:

import re

text = "It is true that string cleaning is a topic in this chapter. string editing is another."
re.findall("string \w+\s", text)
<>:4: SyntaxWarning: invalid escape sequence '\w'
<>:4: SyntaxWarning: invalid escape sequence '\w'
/var/folders/8n/n8x6tb9n5tz72hhnwdf7l04w0000gn/T/ipykernel_75800/970224743.py:4: SyntaxWarning: invalid escape sequence '\w'
  re.findall("string \w+\s", text)
['string cleaning ', 'string editing ']

re.findall() devuelve todas las coincidencias. Hay varias funciones de búsqueda útiles en re que conviene conocer y que tienen una sintaxis similar, re.function(regex, text). La tabla muestra lo que hace cada una

Función Qué hace Ejemplo de uso Resultado para el valor dado de text
re.match() Indica si hay una coincidencia al principio de un string. re.match("string \w+\s" , text) is True None
re.search() Indica si hay una coincidencia en cualquier parte del string. re.search("string \w+\s" , text) is True True
re.findall() Devuelve todas las coincidencias. re.findall("string \w+\s" , text) ['string cleaning ', 'string editing ']
re.split() Divide el texto allí donde hay una coincidencia. re.split("string \w+\s" , text) ['It is true that ', 'is a topic in this chapter. ', 'is another.']

Otra función de regex muy práctica es re.sub(), que sustituye un fragmento de texto por otro si encuentra una coincidencia. Aquí tienes un ejemplo:

new_text = "new text here! "
re.sub("string \w+\s", new_text, text)
<>:2: SyntaxWarning: invalid escape sequence '\w'
<>:2: SyntaxWarning: invalid escape sequence '\w'
/var/folders/8n/n8x6tb9n5tz72hhnwdf7l04w0000gn/T/ipykernel_75800/3231031485.py:2: SyntaxWarning: invalid escape sequence '\w'
  re.sub("string \w+\s", new_text, text)
'It is true that new text here! is a topic in this chapter. new text here! is another.'

Caracteres especiales

Hasta ahora solo hemos visto una aplicación muy sencilla de regex con una palabra simple, string, el código para otra palabra \w+ y el código para un espacio en blanco \s. Veamos de forma más completa los caracteres especiales de regex:

Carácter Descripción Texto de ejemplo Regex de ejemplo Texto coincidente de ejemplo
Un dígito Unicode de cualquier sistema de escritura “file_93 is open” file_\d\d “file_93”
“carácter de palabra”: letra Unicode, dígito o guion bajo “blah hello-word blah” \w-\w “hello-world”
“carácter de espacio en blanco”: cualquier separador Unicode “these are some words with spaces” words\swith\sspaces “words with spaces”
Carácter que no es dígito (lo opuesto a “ABC 10323982328” \D\D\D “ABC”
Carácter que no es de palabra (lo opuesto a ) “Once upon a time *” \W “*”
Carácter que no es espacio en blanco (lo opuesto a ) “y” \S “y”
Final del string “End of a string” \w+\Z “string””
. Coincide con cualquier carácter excepto el salto de línea “ab=def” ab.def “ab=def”

Ten en cuenta que los caracteres de espacio en blanco incluyen los saltos de línea, \n, y los tabuladores, \t.

Cuantificadores

Además de estos caracteres especiales, existen cuantificadores que piden más de una aparición de un carácter. Por ejemplo, en lo anterior, \w\w pedía dos caracteres de palabra, mientras que \d\d pide dos dígitos. La siguiente tabla muestra todos los cuantificadores.

Cuantificador Función Texto de ejemplo Regex de ejemplo Coincidencia de ejemplo
{m} Exactamente m repeticiones “936 and 42 are the codes” \d{3} “936”
{m,n} De m (por defecto 0) a n (por defecto infinito) “Words up to four letters” \b\w{1,4}\b “up”, “to”, “four”
* 0 o más. Equivale a {,} “42 is the code” \d*\s “42”
+ 1 o más. Equivale a {1,} “4 323 hello” \d+ “4”, “323”
? Opcional, es decir, 0 o 1. Equivale a {,1}. “4 323 hello” \d?\s “4”
TipEjercicio

Encuentra una única regex que extraiga solo los números de porcentaje tanto de “Inflation in year 3 was 2 percent” como de “Interest rates were as high as 12 percent”.

Metacaracteres

Además de los caracteres especiales y los cuantificadores, podemos tener coincidencias de metacaracteres. No son caracteres per se, sino inicios, finales y otras partes de las palabras. Por ejemplo, \b encuentra strings en el límite de una palabra (\w+), así que si tomáramos el texto “Three letter words only are captured” y ejecutáramos \b\w\w\w\b, obtendríamos “are”. \B encuentra strings que no están en el límite de una palabra (\w+), así que el texto “Bricks” con \B\w\w\B aplicado daría “ri”. La siguiente tabla contiene algunos metacaracteres útiles.

Secuencia de metacaracteres Significado Regex de ejemplo Coincidencia de ejemplo
^ Inicio del string o de la línea ^abc “abc” (que aparece al inicio del string o de la línea)
$ Final del string o de la línea xyz$ “xyz” (que aparece al final del string o de la línea)
Coincide con el string en el límite de una palabra (+) ing\b “matching” (coincide con ing si está al final de una palabra)
Coincide con el string fuera del límite de una palabra (+) \Bing\B “stinger” (coincide con ing si no está al principio ni al final de la palabra)

Como hay tantos caracteres con significado especial en regex, si quieres buscar, por ejemplo, un signo de dólar o un punto, primero tienes que escapar el carácter con una barra invertida. Así, \${1}\d+ buscaría un único signo de dólar seguido de algunos dígitos y detectaría el ‘$50’ en ‘she made $50 dollars’.

TipEjercicio

Encuentra la regex que extraiga solo la primera aparición de la palabra ‘money’ y cualquier palabra que siga a ‘money’ en el siguiente texto: “money supply has grown considerably. money demand has not kept up.”.

Rangos

Probablemente creas que ya terminaste con las regex, ¡pero no tan rápido! Aún quedan más metacaracteres. Esta vez representarán rangos de caracteres.

Secuencia de metacaracteres Descripción Expresión de ejemplo Coincidencia de ejemplo
[caracteres] Los caracteres dentro de los corchetes forman parte de un conjunto de caracteres coincidentes [abcd] a, b, c, d, abcd
[^…] Los caracteres dentro de los corchetes forman un conjunto no coincidente; cualquier carácter que no esté dentro es un carácter coincidente. [^abcd] Cualquier aparición de cualquier carácter EXCEPTO a, b, c, d.
[carácter-carácter] Cualquier carácter del rango entre dos caracteres (incluidos) forma parte del conjunto [a-z] Cualquier letra minúscula
[^carácter] Cualquier carácter que no sea el carácter indicado [^A] Cualquier carácter EXCEPTO la A mayúscula

Los rangos tienen dos trucos más muy útiles. El primero es que se pueden concatenar. Por ejemplo, [a-c-1-5] coincidiría con cualquiera de a, b, c, 1, 2, 3, 4, 5. También se pueden modificar con un cuantificador, de modo que [a-c0-2]{2} coincidiría con “a0” y “ab”.

Regexes codiciosas frente a perezosas

Prepárate, porque esto es un poco difícil de entender. Añadir un ? después de una regex hará que pase de ser ‘codiciosa’ (greedy) a ser ‘perezosa’ (lazy). Codiciosa significa que obtendrás la coincidencia con el string más largo posible que cumpla la condición. Perezosa significa que obtendrás el string más corto posible que cumpla la condición. Es más fácil demostrarlo con un ejemplo:

test_string = "stackoverflow"
greedy_regex = "s.*o"
lazy_regex = "s.*?o"

print(f"The greedy match is {re.findall(greedy_regex, test_string)[0]}")
print(f"The lazy match is {re.findall(lazy_regex, test_string)[0]}")
The greedy match is stackoverflo
The lazy match is stacko

En el primer caso (codicioso), obtenemos desde una ‘s’ hasta la última ‘o’ dentro de la misma palabra. En el segundo caso (perezoso) solo obtenemos todo lo que hay entre el inicio y la primera aparición de una ‘o’.

Coincidencias frente a grupos de captura

A menudo hay una diferencia entre lo que quieres que coincida y lo que realmente quieres extraer con tu regex. Supongamos, por ejemplo, que estamos analizando un texto y queremos cualquier número que siga el formato ‘$xx.xx’, donde las ‘x’ son números, pero no queremos el signo de dólar. Para ello, podemos crear un grupo de captura usando paréntesis. Aquí tienes un ejemplo:

text = "Product 1 was $45.34, while product 2 came in at $50.00 however it was assessed that the $4.66 difference did not make up for the higher quality of product 2."
re.findall("\$(\d{2}.\d{2})", text)
<>:2: SyntaxWarning: invalid escape sequence '\$'
<>:2: SyntaxWarning: invalid escape sequence '\$'
/var/folders/8n/n8x6tb9n5tz72hhnwdf7l04w0000gn/T/ipykernel_75800/3729052804.py:2: SyntaxWarning: invalid escape sequence '\$'
  re.findall("\$(\d{2}.\d{2})", text)
['45.34', '50.00']

Desglosemos la regex. Primero, pedimos un signo de dólar literal usando \$. A continuación, abrimos un grupo de captura con (. Luego indicamos que solo nos devuelva los números que tengan 2 dígitos, un punto y otros 2 dígitos (excluyendo así $4.66). Por último, cerramos el grupo de captura con ).

Así, aunque especificamos una coincidencia usando regex, solo queremos que al ejecutar la regex se devuelva el grupo de captura.

Veamos un ejemplo más complicado.

sal_r_per = r"\b([0-9]{1,6}(?:\.)?(?:[0-9]{1,2})?(?:\s?-\s?|\s?to\s?)[0-9]{1,6}(?:\.)?(?:[0-9]{1,2})?)(?:\s?per)\b"
text = "This job pays gbp 30500.00 to 35000 per year. Apply at number 100 per the below address."
re.findall(sal_r_per, text)
['30500.00 to 35000']

En este caso, la regex busca primero hasta 6 dígitos, luego opcionalmente un punto, luego opcionalmente otro par de dígitos, después un guion o ‘to’ usando el operador ‘|’ (que significa o), seguido de un número similar, seguido de ‘per’.

Pero el grupo de captura es solo el subconjunto de la coincidencia que corresponde al rango de números; descartamos casi todo lo demás. Observa también que otros números, aunque vayan seguidos de ‘per’, no se recogen. (?:) inicia un grupo de no captura, que solo busca coincidencias pero no captura, de modo que, aunque (?:\s?per) busca ” per” después de un salario (con el espacio opcional gracias al segundo ?), no se devuelve.

TipEjercicio

Encuentra una regex que capture el rango salarial de “Salary Pay in range $9.00 - $12.02 but you must start at 8.00 - 8.30 every morning.”.

Este ha sido un recorrido vertiginoso por las regexes. Aunque una regex parezca un galimatías, es una herramienta muy útil para tareas más complejas de limpieza y extracción de strings.