Hamburger Icon
Python y Expresiones Regulares: Desvelando Trucos para Texto que Transformarán tu Código

Python y Expresiones Regulares: Desvelando Trucos para Texto que Transformarán tu Código

¿Te has preguntado alguna vez cómo los expertos manipulan el texto de manera tan eficiente en Python? Las expresiones regulares son la clave. Y lo bueno, es que es más fácil de lo que parece. Este artículo te guiará a través de trucos prácticos que transformarán tu forma de programar. ¡Prepárate para mejorar tus habilidades y hacer tu código más awesómico que nunca!

Lo que vamos a hacer es, en primer lugar, resumir brevemente qué son las expresiones regulares y a explicar las herramientas básicas con las que vamos a trabajar.

Despuúes, nos pondremos en la piel de Jack el Destripador. Veremos cuatro ejemplos reales en los que podremos usar expresiones regulares, cortando trocito a trocito todo lo que sucede desde el lado de Python para interpretar la expresión regular que definiremos.

Vamos allá.

Qué son las Expresiones Regulares

Son secuencias de caracteres que forman un patrón de búsqueda. La palabra clave: patrón. ¿Pero para qué queremos un patrón? Pues para buscar, reemplazar o validar texto en una cadena. Le encontrarás utilidad en los ejemplos que te muestro más abajo.

Y ya está. ¿Has visto que rápido? Este post es poca teoría y mucha práctica. Vamos a empezar ya.

Sintaxis Básica

Para usar expresiones regulares en Python es necesario importar el módulo re, declarar el patrón en una cadena (con la "r" delante) y realizar la búsqueda:

import re

pattern = r"abc"  # Este es un patrón simple que busca la secuencia "abc" en una cadena.

# Ahora, utilizamos el método search para encontrar el patrón en una cadena.
result = re.search(pattern, "xyzabc123")

if result:
    print("Encontrado")
else:
    print("No encontrado")

Esto es lo más sencillo. El caso es que para crear un patrón un poquito más complejo, es importante conocer los caracteres especiales que puedes usar, los cuantificadores y las clases de caracteres.

Caracteres Especiales

Existen caracteres especiales que, en una expresión regular, tienen un significado específico y se usan para darle dinamismo. Son estos:

  • .: el punto es un comodín que implica que puede coincidir con cualquier carácter, excepto con una nueva línea.
  • ^: el caret o circunflejo se puede usar al inicio de un patrón para indicar que debe coincidir con el inicio de la cadena.
  • $: el símbolo de dolar es parecido al anterior, pero se usa al final de un patrón, para indicar que debe coincidir con el final de la cadena.

Otros caracteres especiales son estos:

  • \d: representa cualquier dígito del 0 al 9.
  • \w: representa cualquier palabra alfanumérica, incluyendo guión bajo "_".
  • \W: negación del anterior: cualquier palabra que NO sea alfanumérica. Dicho de otro modo, coincide con caracteres especiales y espacios en blanco.
  • \s: representa cualquier carácter de espacio en blanco (espacio, tabulación, nueva línea...).
  • \S: negación del anterior: cualquier carácter que NO sea de espacio en blanco.
  • \b: representa un límite inicial o final de una palabra.

Puede ser que ahora mismo todo esto parezca confuso. No te preocupes, se verá mucho más claro con los ejemplos que destriparemos junto a Jack.

Cuantificadores y Clases de Caracteres

Los cuantificadores nos indican el número de repeticiones permitidas:

  • *: el asterisco nos sirve para indicar 0 o más repeticiones.
  • +: el símbolo de suma es para indicar 1 o más repeticiones.
  • ?: el interrogante es para indicar solo 0 o 1 repetición.

Estos son los cuantificadores generales, pero puedes personalizarlos así:

  • {n}: coincidirá con cualquier secuencia de n repeticiones.
  • {n,}: coincidirá con secuencias de n repeticiones o más.
  • {n,m}: coincidirá con secuencias de entre n y m repeticiones.

Y sobre las clases de caracteres, simplemente mencionar que se agrupan entre corchetes para especificar un conjunto concreto, como [aeiou] que coincidirá con cualquier vocal, o [A-Z], que coincidirá con cualquier letra mayúscula o [0-9] que coincidirá con cualquier dígito. Dentro de una clase puedes usar guiones para denotar rangos de caracteres, además, también las puedes combinar con los cuantificadores para crear patrones más complejos.

Insisto igual que antes, aunque ahora parezca confuso cuando veas los ejemplos se entenderá mucho mejor.

Ejemplos Prácticos

Vamos a juntar los conceptos vistos hasta ahora y a experimentar de verdad con expresiones regulares.

Para hacerlo, jugaremos a ser Jack el Destripador. Cogeremos los patrones y los destriparemos sin piedad. Vas a entender cada trocito del patrón, para ello marcaré en negrita la parte que interese en cada momento.

Validar una Dirección de Email

import re


def validar_email(email):
    patron = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    if re.match(patron, email):
        return True
    return False


# Ejemplo de uso
correo = "usuario@dominio.com"
if validar_email(correo):
    print(f"{correo} es un correo electrónico válido.")
else:
    print(f"{correo} no es un correo electrónico válido.")

¡Uf! Menudo chorro de patrón. Antes de continuar, te animo a que, una vez vista la teoría de más arriba, intentes descifrarlo a ver qué partes entiendes y cuáles no.

Seguramente puedas intuir algo así como una parte del patrón que verifica el nombre de usuario, seguido de una arroba, seguido de otro patrón para verificar el nombre del dominio. Si es así, estás en lo cierto.

Vamos a destriparlo. Para ello, lo primero es separar las partes. Ya sabes, eso de divide y vencerás.

La primera parte es lo que corresponde al nombre de usuario, esta: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$"

Aquí están sucediendo varias cosas:

  1. Con el circunflejo "^" indicamos que el email debe empezar con la clase de caracteres especificada.
  2. Tenemos una clase de caracteres "[a-zA-Z0-9._%+-]+" que termina con el símbolo de suma "+", y esto nos indica que la parte anterior tiene 1 o más repeticiones.
  3. La clase de caracteres "[a-zA-Z0-9._%+-]" nos está diciendo que podemos encontrar cualquier carácter alfanumérico, cuyas letras pueden ser mayúsculas o minúsculas y que pueden ser también caracteres especiales como el punto, el guión medio, porcentaje, símbolo de suma o guión bajo.

Si lo pensamos un poco, eso es lo que define el nombre de usuario en un correo elctrónico, una consecución de 1 o más caracteres que se encuentren dentro de ese rango de posibilidades (las indicadas en el punto 3).

La segunda parte del patrón es la arroba "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$", simplemente nos está diciendo que después del nombre de usuario viene símbolo, ni más ni menos.

La tercera parte es el dominio y la podemos dividir en el nombre de dominio y su extensión (separados por un punto). Vamos a ver la parte del nombre de dominio: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$":

  1. Esta clase de caracteres también termina con "+", por lo que lo anterior se puede repetir 1 o más veces.
  2. La clase de caracteres nos dice que el nombre del dominio puede contener caracteres alfanuméricos, el símbolo de punto y el símbolo de guión.

Luego, tenemos un punto, para separar el nombre de la extensión del dominio: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$".

Y finalmente, la extensión, definida por esta parte del patrón: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$". En esta parte destacamos lo siguiente:

  1. Con el símbolo de dolar "$" del final, estamos indicando que la cadena de caracteres que forman el correo electrónico debe finalizar con esta última parte.
  2. La clase de caracteres nos indica que la extensión solo puede contener letras mayúsculas y minúsculas.
  3. La clase de caracteres está seguida de "{2,}", lo que nos está diciendo que la extensión debe contener 2 letras o más.

No me dirás que una vez destripado continua siendo igual de complejo que antes, ¿no? La clave es cortar en cachos más pequeños que juntos conforman un significado global. La experiencia te dirá por dónde cortar.

Extraer Números de Teléfono de un Texto

Hasta ahora hemos usado el método match() del módulo re para verificar un patrón. En este ejemplo vamos a usar el método findall(), que nos devuelve un listado de coincidencias con el patrón indicado.

import re


def extraer_numeros_telefono(texto):
    # Patrón para números de teléfono en España
    patron = r'\b(?:\+34|34|0034)?[6789]\d{8}\b'
    return re.findall(patron, texto)


# Ejemplo de uso
texto = "Mi número es +34600112233 y el de mi amigo es 912345678. Llámame al fijo 912345678."
numeros_telefono = extraer_numeros_telefono(texto)

print("Números de teléfono encontrados:", numeros_telefono)

Ya ves que este patrón es algo diferente al del ejemplo anterior. Pero no pasa nada, ya sabes lo que hay que hacer: ir por partes. Las condiciones a tener en cuenta para los números de teléfono en España son:

  • Empiezan con 6 o 7 si son móviles
  • Empiezan con 8 o 9 si son fijos
  • Pueden ir precedidos del prefijo 34, +34 o 0034 en llamadas internacionales
  • Sin contar el prefijo, tienen una longitud total de 9 cifras

la primera parte es muy pequeñita, se trata de un carácter especial: "\b(?:+34|34|0034)?[6789]\d{8}\b". El "\b" nos asegura que la coincidencia coincida con el límite inicial de la palabra.

Luego, tenemos este otro trocito que revisa el prefijo "\b (?:\+34|34|0034)?[6789]\d{8}\b". He hecho trampa porque ahora veremos un concepto nuevo que no te he explicado antes.

Esta cosa "(?: ...)" es un grupo no capturador, que sirve para agrupar unas opcioens sin capturar la coincidencia para su posterior referencia. Es decir, estamos creando un grupo para contener las opciones, pero no queremos recuperar la coincidencia de ese grupo, simplemente lo usaremos como referencia.

Por lo tanto, con "(?:\+34|34|0034)?" lo que hacemos es:

  1. Estamos creando el grupo no capturador que contenga "+34" (fíjate que el símbolo de suma debe escaparse con "\"), 34 o 0034
  2. Cada opción del grupo la separamos con el operador lógico or "|".
  3. El interrogante del final indica que toda la expresión anterior, esto es, el grupo no capturador, es opcional y puede aparecer 0 o 1 vez.

La siguiente parte ya es la final "\b(?:+34|34|0034)?[6789]\d{8}\b" y seguramente ya vayas intuyendo que lo que está sucendiendo es:

  1. Que la clase de caracteres limita los valores con los que puede empezar el primer dígito del número de teléfono.
  2. Que el "\d{8}" implica que los siguientes 8 dígitos pueden ser cualquiera entre 0 y 9.
  3. Que con \b nos aseguramos la coincidencia con el límite final de la palabra.

¿Fácil? Bueno, en este ejemplo has visto cómo funciona un grupo no capturador pero el resto era más asequible revisando la teoría del principio.

Extraer URLs de un Texto

Este ejemplo es bastante parecido al anterior, te animo a que intentes crear la expresión regular necesaria antes de continuar, ya que la práctica es lo que va a hacer que ganes solvencia con esto.

import re


def extraer_urls(texto):
    patron = r'\b(?:https?|ftp):\/\/\S+\b'
    return re.findall(patron, texto)


# Ejemplo de uso
texto = "Visita mi sitio web en https://www.ejemplo.com o mira este enlace ftp://archivos/ejemplo.txt"
urls_encontradas = extraer_urls(texto)

print("URLs encontradas:", urls_encontradas)

Vale, en este caso vamos a capturar todas las URLs que usen el protocolo "https", "http" y "ftp". Esto podrías complicarlo hasta dónde tu quieras, ya que existen otros protocolos posibles, pero para aprender expresiones regulares ya vamos servidos 😁.

Al igual que con el ejemplo anterior, al inicio y al final usamos el carácter especial para delimitar inicio y fin de palabra: "\b(?:https?|ftp):\/\/\S+\b".

Entonces, este patrón se divide en dos partes más. La primera es esta "\b (?:https?|ftp):\/\/\S+\b" y aquí suceden varias cosas interesantes que, con lo visto hasta ahora, ya puedes descifrar:

  1. Tenemos un grupo no capturador con las opciones "https?" y "ftp". En el primero, el interrogante final implica que el carácter anterior, la "s", puede estar o no estar.
  2. Después del grupo no capturador para hacer referencia al protocolo de la URL, la cadena debe tener "://", dónde se han escapado las barras "/".

La segunda parte "\b(?:https?|ftp):\/\/\S+\b" es más cortita, y simplemente estamos buscando cualquier carácter que no sea un espacio en blanco y dicho patrón se repita 1 o más veces.

Como ejercicio, puedes intentar mejorar el patrón para que revise la extensión del dominio como en el primer ejemplo de verificación de correos electrónicos.

Ocultar Números de Tarjeta de Crédito

Muchas veces se te muestran los números de una tarjeta de modo que solo puedes ver los últimos cuatro dígitos, donde el resto son asteriscos. Podemos crear una función que oculte información de tarjetas de crédito con expresiones regulares. Aunque seguro que se te ocurre una forma mucho más sencilla de hacerlo sin usarlas, aquí estamos tratando de practicar.

En este ejemplo veremos algo nuevo, el uso del la función sub() para hacer el reemplazo de números por asterisco..

import re


def ocultar_tarjetas(texto):
    patron = r'\b(\d{4}-){3}\d{4}\b'
    def ocultar(match):
        # Función para ocultar todos los dígitos excepto los últimos cuatro.
        return '*' * (len(match.group()) - 4) + match.group()[-4:]

    resultado = re.sub(patron, ocultar, texto)
    return resultado


# Ejemplo de uso
texto_con_tarjeta = "Mi número de tarjeta es 1234-5678-9012-3456."
texto_oculto = ocultar_tarjetas(texto_con_tarjeta)

print(texto_oculto)

Ante todo, vamos a ver claramente qué está sucediendo a nivel algorítmico. Cuando se llama a la función ocultar_tarjetas() lo que sucede es:

  1. Se define el patrón
  2. Se define una función de ámbito interno ocultar() que recibe la coincidencia y devuelve la cadena con los asteriscos y los cuatro dígitos
  3. Se usa la función sub() del módulo re pasándole el patrón, la función de ocultación/sustitución y la tarjeta que recibe como parámetro

En esta ocasión vamos a destripar doblemente, hoy Jack se pone las botas. Primero destriparemos el uso de la función sub() y de postre, el patrón.

La función sub() se utiliza para realizar sustituciones de patrones en una cadena. Recibe tres argumentos:

  • Patrón: expresión regular que quieres buscar en la cadena.
  • Reemplazo: cadena que se utilizará para reemplazar las coincidencias del patrón.
  • Cadena: string original en la que queremos realizar sustituciones

En nuestro caso, usamos una función como argumento de reemplazo. Cuando hacemos esto, la función que le pasamos a sub() debe aceptar como argumento una variable ("match" en este caso) que represente el objeto de coincidencia. La función sub() se encarga de poner ese parámetro.

En la función ocultar() se usa el método group(), que simplemente devuelve la cadena que coincide con el patrón.

Entonces, cuando re.sub encuentra una coincidencia del patrón en la cadena original, llama a la función ocultar() con un objeto de coincidencia como argumento, y la función devuelve la cadena modificada que se utiliza para realizar el reemplazo en la cadena original.

Ahora, el postre, vamos a desgranar el patrón "\b(\d{4}-){3}\d{4}\b". Esta vez lo vamos a dividir en tres partes. La primera es como alguna otra que ya hemos visto, el uso del carácter especial "\b" para poner los límites iniciales y finales de la palabra "\b(\d{4}-){3}\d{4}\b".

Luego tenemos esta otra parte "\b (\d{4}-){3}\d{4}\b":

  1. Si antes hemos usado un grupo no capturador, en esta ocasión usamos un grupo capturador, que como us nombre indica, captura bloques de 4 dígitos con un guión al final.
  2. Cada uno de estos bloques debe repetirse 3 veces.

Finalmente, tenemos esta parte "\b(\d{4}-){3}\d{4}\b", que es la coincidencia con los 4 últimos dígitos de la tarjeta.

Espero que Jack haya quedado, de una vez por todas, saciado.

Conclusiones

En este post has podido experimentar el placer que siente Jack el Destripador cuando hace su trabajo.

Después de la introducción teórica necesaria para empezar a trabajar con expresiones regulares, has podido ver el desmembramiento de varios patrones a partir de cuatro ejemplos diferentes aplicables al día a día de cualquier developer.

Por el camino has aprendido otros conceptos como grupos capturadores y no capturadores. Has podido poner en práctica la definición y estudio de expresiones regulares y has visto cómo resolver problemas de la vida real.

Ya solo te queda una cosa: prácticar más.

Es cuestión de tiempo que toda persona que se dedique a desarrollar software se encuentre con la necesidad de definir o interpretar expresiones regulares, lo que a veces supone un problema, por lo que, aunque pueda parecer tedioso, es importante conocer una forma de encarar este obstáculo.

Para ello, Jack el Destripador nos ha legado el mejor patrón a seguir.