Expresiones Regulares
La biblioteca re básicamente nos permite buscar y coincidir con patrones específicos de texto. Lo primero que debemos hacer es importar el módulo re, que significa "expresiones regulares" (regular expressions).
Antes de ejecutar nuestra primera expresión regular, necesitamos saber qué es una cadena cruda (raw string).
Cadenas Crudas (Raw Strings)
Una cadena cruda es simplemente una cadena de texto precedida por una "r". Esto le indica a Python que no trate las barras invertidas (\) de ninguna manera especial. Normalmente, las barras invertidas se usan para especificar tabulaciones o saltos de línea. Por ejemplo, si uso la expresión print("\t"), se crea una tabulación, pero si uso una cadena cruda, interpretará la cadena literalmente.
# Sin cadena cruda
print("\tTab")
# Con cadena cruda
print(r"\tTab")
Compilar Patrones
Primero que nada, importamos el módulo re:
import re
Para escribir nuestros patrones, necesitamos usar el método compile, el cual nos permite separar nuestros patrones en una variable y también facilita la reutilización de esa variable para realizar múltiples búsquedas.
Por ejemplo, imagina que queremos analizar una cadena y encontrar una coincidencia para "abc".
pattern = re.compile(r'abc')
Finditer (Encontrar iteradores)
Ahora que tenemos un patrón especificado, podemos buscar a través de un texto ese patrón.
matches = pattern.finditer(text_to_search)
El argumento text_to_search es la cadena donde queremos buscar todas las coincidencias. La variable matches será un iterador.
for match in matches:
print(match)
El código anterior imprimirá todas las coincidencias de esta manera:
<re.Match object; span=(0, 3), match='abc'>
El span (intervalo) indica el índice de inicio y fin de la coincidencia. Este código no coincide con las letras mayúsculas "ABC", y eso se debe a que la búsqueda es sensible entre mayúsculas y minúsculas (case sensitive).
Existen caracteres especiales en la biblioteca re llamados metacaracteres. Estos caracteres son especiales porque tienen propósitos específicos para coincidir con un patrón. Por ejemplo, si queremos buscar un punto (.), haremos match con todos los carácteres, dado que el punto, para las expresiones regulares, es un metacaracter que índica "todos los caracteres".
Es por eso que esos caracteres necesitan ser "escapados", y podemos hacerlo usando una barra invertida (\).
pattern = re.compile(r'\.') # --> Correcto
pattern = re.compile(r'.') # --> Incorrecto
Aunque una búsqueda literal usando escapes no es muy emocionante (porque es algo que podemos hacer perfectamente sin usar la biblioteca re), podemos usar esta biblioteca para buscar patrones. Para hacer esto, usaremos algunos de los metacaracteres, como el punto que acabamos de ver.
Metacaracteres
Los metacaracteres se utilizan para coincidir con patrones.
| Metacarácter | Descripción |
|---|---|
. |
Cualquier carácter excepto una nueva línea |
\d |
Dígito (0-9) |
\D |
No es un dígito (0-9) |
\w |
Carácter de palabra (a-z, A-Z, 0-9, _) |
\W |
No es un carácter de palabra |
\s |
Espacio en blanco (espacio, tabulación, nueva línea) |
\S |
No es un espacio en blanco |
Anclas (Anchors)
Coinciden con posiciones invisibles (de longitud cero) antes o después de los caracteres. Podemos usarlas en conjunto con los metacaracteres.
| Ancla / Símbolo | Descripción |
|---|---|
\b |
Límite de palabra* (espacio en blanco o carácter no alfanumérico) |
\B |
No es un límite de palabra |
^ |
Inicio de una cadena |
$ |
Fin de una cadena |
[] |
Coincide con caracteres entre corchetes** |
[^ ] |
Coincide con caracteres NO entre corchetes |
| |
O (Either Or) |
( ) |
Grupo |
*Nota sobre los límites de palabra (
\b): Un límite de palabra puede ocurrir en una de tres posiciones:
- Antes del primer carácter de la cadena, si el primer carácter es un carácter de palabra.
- Después del último carácter de la cadena, si el último carácter es un carácter de palabra.
- Entre dos caracteres de la cadena, donde uno es un carácter de palabra y el otro no lo es.
**Nota sobre los conjuntos de caracteres (
[]): Esto se llama conjunto de caracteres (character set) y utiliza corchetes con los caracteres con los que queremos coincidir. Por ejemplo, si queremos coincidir con un guion o un punto, escribimos[-.]. No es necesario escapar esos caracteres porque están dentro de los corchetes. Esto solo coincide con un carácter a la vez, lo que significa que si tengo dos guiones seguidos (--), no se considerará como una coincidencia.
También podríamos especificar un rango de valores usando un guion. Por ejemplo, sabemos que \d solo coincide con dígitos, pero si solo queremos coincidir con dígitos entre el 1 y el 5, deberíamos escribir [1-5]. Con las letras funciona de la misma manera.
Si queremos coincidir con más de un rango, podemos hacerlo fácilmente escribiendo todos los rangos que queremos, por ejemplo [a-zA-Z]. Así, el ejemplo coincidirá con todas las letras mayúsculas y minúsculas.
Cuantificadores (Quantifiers)
Podemos usar cuantificadores para coincidir con más de un carácter a la vez. Van después del metacarácter o del metacarácter escapado.
| Cuantificador | Descripción |
|---|---|
* |
0 o más |
+ |
1 o más |
? |
0 o 1 |
{3} |
Número exacto |
{3,4} |
Rango de números (Mínimo, Máximo) |
Ejemplos Prácticos
Ejemplo 1: Si queremos buscar números de teléfono como 321-555-4321 o 123.555.1234, necesitaríamos coincidir con dígitos (\d). Sin embargo, el carácter separador podría ser cualquier cosa, por lo que es más conveniente usar un punto.
Para coincidir con esto, deberíamos seguir el siguiente patrón: \d\d\d.\d\d\d.\d\d\d\d
Usando cuantificadores se vería así: \d{3}.\d{3}.\d{4}
Si no sabemos cuántas repeticiones tienen nuestros patrones, podríamos utilizar otros cuantificadores.
Ejemplo 2: Imagina que quieres coincidir con todos los "Mr" y "Misses" en todas sus formas:
- Mr. Schafer
- Mr Smith
- Ms Davis
- Mrs. Robinson
- Mr. T
Tenemos algunos puntos en algunos de los nombres y en otros no. Por eso necesitamos poner el punto como una coincidencia opcional usando el cuantificador de interrogación (?).
El problema es que tenemos Ms, Mrs y Mr, y son bastante diferentes entre sí. Por eso, la mejor manera de resolver esto es usar grupos de patrones. Para hacer un grupo usamos paréntesis:
re.compile(r'(Mr|Ms|Mrs)\.?\s[A-Z]\w*')
Aquí estamos indicando que coincida con todos los Mr, Ms o Mrs, porque el signo | es equivalente a un "o".
Leer expresiones regulares escritas por otras personas es probablemente una de las partes más difíciles de todo esto, pero si lo analizas paso a paso deberías ser capaz de desglosarlo.
Por ejemplo, para coincidir con todos los correos electrónicos posibles tenemos el siguiente ejemplo. Intenta leerlo como ejercicio:
pattern = re.compile(r'[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+')
Usar Grupos para Capturar Información
Imagina que tenemos una serie de URLs y queremos coincidir con todas ellas:
urls = '''
https://www.google.com
http://coreyms.com
https://youtube.com
https://www.nasa.gov
'''
Para coincidir con ellas usamos el siguiente patrón:
pattern = re.compile(r'https?://(www\.)?\w+\.\w+')
El punto aquí es usar nuestros grupos para capturar cierta información, así que capturemos el nombre de dominio y el dominio de nivel superior (top-level domain como .com o .gov). Primero, debemos rodear con paréntesis todas las secciones que queremos capturar:
pattern = re.compile(r'https?://(www\.)?(\w+)(\.\w+)')
Ahora tenemos tres grupos diferentes: el www opcional, el nombre de dominio y el dominio de nivel superior. También hay un grupo 0 que es todo lo que capturamos (la url entera).
La variable match que declaramos antes en el bucle tiene un método llamado group y podemos pasarle el índice del grupo que queremos ver.
for match in matches:
print(match.group(0))
Si imprimimos eso, deberíamos obtener la url entera (grupo 0). Si usamos el índice 1 en su lugar, debería aparecer el www opcional, con el índice 2 debería aparecer el nombre de dominio, y para el 3 debería aparecer el dominio de nivel superior.
El módulo de expresiones regulares tiene un método sub que podemos usar para realizar una sustitución. La estructura de un método sub es algo así:
new_variable = pattern.sub(nuevo_texto_para_reemplazar, texto_original)
Es posible que queramos reemplazar estas URLs solo con el nombre de dominio y el dominio de nivel superior. Para eso, tenemos que usar el primer argumento para poner el nuevo texto referenciando los grupos 1 y 2 (que en nuestro patrón anterior corresponden a los índices 2 y 3) de esta manera:
subbed_url = pattern.sub(r'\2\3', urls)
Así que aquí estamos reemplazando todas las coincidencias de urls por el grupo 2 y 3, por lo que ahora en lugar de https://www.google.com tenemos google.com.
Otros Métodos Importantes
Método findall
Devolverá las coincidencias como una lista de cadenas. Si está coincidiendo con grupos, solo devolverá los grupos.
matches = pattern.findall(text_to_search)
Si queremos encontrar múltiples grupos, devolverá una lista de tuplas y las tuplas contendrán todos los grupos. Si no hay grupos, simplemente devolverá todas las coincidencias en una lista de cadenas.
matches = pattern.findall(r'\d{3}.\d{3}.\d{4}')
El código anterior imprimirá una lista de todos los números de teléfono que coincidan con ese patrón.
Método match
Este método determinará si la expresión regular coincide al principio de la cadena. match no devuelve un iterable como finditer o findall. Solo devuelve la primera coincidencia y si no hay coincidencia, entonces devuelve None.
sentence = 'Start a sentence and then bring it to the end'
pattern = re.compile(r'Start')
matches = pattern.match(sentence)
print(matches)
# Salida:
# <re.Match object; span=(0, 5), match='Start'>
Si busco la palabra 'sentence' en su lugar, el método devolverá None porque solo devuelve coincidencias que están al principio.
Método search
Es lo mismo que el método match con la excepción de que busca en toda la cadena en lugar de solo al principio.
sentence = 'Start a sentence and then bring it to the end'
pattern = re.compile(r'sentence')
matches = pattern.search(sentence)
print(matches)
# Salida:
# <re.Match object; span=(8, 16), match='sentence'>
Si buscamos algo que no coincide, también devolverá None
Banderas (Flags)
Una bandera es una forma de facilitar las cosas cuando trabajamos con expresiones regulares. Digamos que queremos coincidir con una palabra, sin importar si está en mayúscula, minúscula o una mezcla de ambas.
Para hacer esto, es posible que queramos usar una bandera para ignorar mayúsculas y minúsculas (ignore case flag). Veamos un ejemplo de cómo funciona esto:
sentence = 'Start a sentence and then bring it to the end'
pattern = re.compile(r'start', re.IGNORECASE)
El texto que estamos buscando parece estar en mayúscula (Start), pero con esta bandera ignoramos esto y coincidirá de todos modos.
Hay una forma abreviada para estas banderas, así que en lugar de usar re.IGNORECASE podríamos usar re.I.
En resumen
Las expresiones regulares (a través del módulo re de Python) son una herramienta indispensable para analizar, validar, buscar y extraer información dentro de cadenas de texto. Aunque su sintaxis puede parecer un poco críptica al principio, el flujo de trabajo básico es siempre el mismo:
- Definir el patrón utilizando cadenas crudas (
r"...") para evitar problemas con los caracteres de escape. - Compilar el patrón con
re.compile()para optimizar el rendimiento y hacer el código más limpio. - Buscar utilizando el método que mejor se adapte a tu necesidad (
searchpara la primera coincidencia,findallofinditerpara obtener múltiples resultados, ymatchpara verificar solo el inicio del texto). - Extraer o manipular los datos aprovechando los grupos de captura
()y métodos comosub()para reemplazar texto.
Dominar el uso de metacaracteres, anclas y cuantificadores requiere un poco de práctica, pero te permitirá transformar horas de procesamiento de texto manual en unas pocas líneas de código rápido y eficiente.
Deja un comentario