Aprende leyendo en orden

Expresiones regulares re — Búsqueda y reemplazo de patrones

Aprende el módulo re de Python desde cero. Cubre cuándo recurrir a re.match / re.search / re.findall, combinar metacaracteres \d / \w / \s / * / + / ?, capturar grupos con ( ), sustitución con re.sub, y reutilizar patrones con re.compile — con ejercicios prácticos ejecutables.

Este artículo recorre el módulo re para expresiones regulares«extraer y reemplazar las subcadenas que coinciden con un patrón específico». Cosas que haces constantemente en proyectos reales — analizar números de teléfono, emails, líneas de log y URLs — se vuelven one-liners.

Una herramienta para probar regex en vivo

Las expresiones regulares tienen muchas piezas en juego y son difíciles de razonar puramente en la cabeza. Para verificar si tu patrón coincide con lo que quieres, el Regex Extractor corre completamente en el navegador — escribe un patrón y algo de texto y mira las coincidencias en tiempo real. Mantenerlo abierto junto a este artículo hace mucho más fácil seguir.

match, search y findall — Tres funciones de búsqueda y cuándo usar cada una

El módulo re expone varias funciones de búsqueda, y eliges entre tres dependiendo de qué necesites. Los nombres son descriptivos — match coincide al inicio, search busca una coincidencia en cualquier lugar, y findall las encuentra todas. El rango de búsqueda exacto, tipo de retorno y comportamiento sin coincidencia se resumen en la siguiente tabla.

FunciónRango de búsquedaRetornoSin coincidencia
re.matchSolo el inicio de la cadenaMatch objectNone
re.searchPrimera coincidencia en cualquier lugarMatch objectNone
re.findallTodas las coincidenciasLista de cadenasLista vacía []

Del Match object que devuelven re.match y re.search (un objeto que contiene la posición del match, la cadena coincidente y la info de grupos), lees la cadena coincidente llamando su método `.group()`m.group() o m.group(0) para el match completo, y (con los grupos de captura introducidos más adelante) m.group(1) para solo lo que estaba dentro de los paréntesis. Solo re.findall devuelve una lista directamente, así que no llamas .group() sobre ella.

Cómo difieren re.match / search / findall
re.matchmatch desde el inicioSi el inicio no coincide→ Nonere.searchprimer match en cualquier lugarMatch si encontradoNone si nore.findalltodas las coincidenciasLista de cadenas coincidentes([] si ninguna)
match solo verifica si el patrón aparece al inicio de la cadena. search devuelve la primera coincidencia en cualquier posición. findall devuelve todas las coincidencias en una lista.
MetacarácterSignificadoEjemplo
\dUn solo dígito (0-9)\d+ → uno o más dígitos
\wUn carácter de palabra (alfanumérico + guion bajo)\w+ → IDs y palabras clave
\sUn carácter de espacio (espacio / tab / salto de línea)Separadores
.Cualquier carácter excepto salto de líneaComodín
*Cero o más del anteriora* → vacío también vale
+Uno o más del anteriora+ → al menos uno
?Cero o uno del anteriorOpcional
[abc]Uno entre a / b / cElección
^ / $Inicio / fin de cadenaAnclas
import re

text = "user_id: 12345, age: 30"

# match: desde el inicio (\w+ es una secuencia de caracteres de palabra)
m = re.match(r"\w+", text)
print(m.group())            # user_id

# search: primera secuencia de dígitos en cualquier lugar
s = re.search(r"\d+", text)
print(s.group())            # 12345

# findall: cada secuencia de dígitos
nums = re.findall(r"\d+", text)
print(nums)                 # ['12345', '30']

Escribe regex como una raw string r"..."

Las contrabarras aparecen por todos lados en regex. Una cadena normal "\d" puede tener sus escapes interpretados por la capa de string antes de que re la vea, así que es más seguro escribir la raw string `r"\d"` con la r al inicio. Los editores también suelen resaltar las raw strings como regex, lo que mejora la legibilidad.

Saca un ID y números de una línea de log. Prueba re.match / re.search / re.findall contra la misma cadena y observa cómo difieren los resultados.

① Importa re.

② Define text = "order_id: 9876, qty: 3, price: 1500".

③ Saca una secuencia de caracteres de palabra desde el inicio de la cadena e imprímela como match: ◯◯.

④ Saca la primera secuencia de dígitos de la cadena e imprímela como search: ◯◯.

⑤ Saca cada secuencia de dígitos como una lista e imprímela como findall: ◯◯.

(Si tu código se ejecuta correctamente, aparecerá la explicación.)

Editor Python

Ejecutar el código para ver el resultado

Grupos de captura — Sacar partes específicas de un patrón

Cualquier cosa que pongas dentro de `( )` dentro de un regex se vuelve un grupo de captura — en lugar de solo el match completo, puedes sacar cada pieza por separado. Patrones como r"#(\d+) on (\d{4})-(\d{2})-(\d{2})" te permiten dividir un número de pedido y una fecha de una línea de log de un tirón.

Llama `.group(N)` sobre el Match object para leer el N-ésimo grupo (numerado desde 1). .group(0) (o .group() sin argumentos) devuelve el match completo.

Cómo funcionan los grupos de captura
#(\d+) on(\d{4})-(\d{2})-(\d{2}).group(0)match completo.group(1)número de pedido.group(2)año.group(3)mes
Cada `( )` en el regex se vuelve un grupo, direccionable por índice basado en 1 como .group(1) / .group(2). .group(0) es el match completo.
import re

text = "Order #1234 placed on 2024-03-15"

# Lo que significa el patrón:
#   #         → '#' literal
#   (\d+)     → uno o más dígitos → group(1) número de pedido
#   placed on → 'placed on' literal
#   (\d{4})   → 4 dígitos → group(2) año
#   (\d{2})   → 2 dígitos → group(3) mes
#   (\d{2})   → 2 dígitos → group(4) día
m = re.search(r"#(\d+) placed on (\d{4})-(\d{2})-(\d{2})", text)
if m:
    print("whole:", m.group(0))    # #1234 placed on 2024-03-15
    print("order #:", m.group(1))   # 1234
    print("year:", m.group(2))      # 2024
    print("month:", m.group(3))     # 03
    print("day:", m.group(4))       # 15

Llamar .group() cuando Match es None lanza error

Cuando re.search no encuentra el patrón devuelve None. Llamar m.group() sobre eso crashea con AttributeError: 'NoneType' object has no attribute 'group'. Siempre comprueba con `if m:` primero antes de .group(), o haz ambos en un paso usando el operador morsa: if m := re.search(...): ....

Divide una dirección de email en nombre de usuario y dominio. Usa grupos de captura para sacar ambas partes en una sola búsqueda.

① Importa re.

② Define text = "contáctanos en alice@example.com".

③ Escribe un patrón de email que capture lo que está a cada lado del @ como grupos separados.

- Izquierda: caracteres de palabra más ., +, -, uno o más

- Derecha: el mismo tipo de caracteres, terminando con un dominio como .com

④ Cuando se encuentre el match, imprime `username: ◯◯` y `domain: ◯◯`.

Editor Python

Ejecutar el código para ver el resultado

re.sub — Reemplazar coincidencias de patrón

«Enmascarar PII de un log», «quitar etiquetas HTML y mantener el cuerpo de texto», «normalizar una mezcla de espacios de ancho completo y medio» — todos se reducen a «reescribir cualquier cosa que coincida con un patrón en otra cosa». El replace de string solo maneja subcadenas fijas, pero re.sub lo hace por patrón.

`re.sub(patrón, reemplazo, original)` devuelve una nueva cadena con cada coincidencia reemplazada por el reemplazo. La cadena original está sin cambios (las cadenas Python son inmutables, así que siempre trabajas con el valor de retorno).

Cómo funciona re.sub
Cadena original"Tel: 03-1234-5678"re.sub(\d, *, ...)Nueva cadena"Tel: **-****-****"
Devuelve una nueva cadena con cada coincidencia reemplazada por la cadena de reemplazo. El original es inmutable; recibe el resultado vía el valor de retorno.
import re

# Enmascarar dígitos en un número de teléfono (reemplazar cada \d con un *)
text = "Tel: 03-1234-5678"
masked = re.sub(r"\d", "*", text)
print(masked)
# Tel: **-****-****

# Quitar etiquetas HTML para quedarse solo con el cuerpo de texto
html = "<p>Hola <b>Mundo</b></p>"
plain = re.sub(r"<[^>]+>", "", html)
print(plain)
# Hola Mundo

Enmascara los dígitos de cualquier número de teléfono que aparezca en una línea de log reemplazándolos con ``.**

① Importa re.

② Define text = "Contacto: 03-1234-5678 o 090-9999-8888".

③ Usa re.sub para *reemplazar cada dígito `\d` con un solo `**, e imprime el resultado como masked: ◯◯`.

④ Imprime el text original de nuevo como original: ◯◯ para confirmar que está sin cambios (re.sub solo devuelve una nueva cadena).

Editor Python

Ejecutar el código para ver el resultado

re.compile — Reutilizar un patrón

Cuando usas el mismo regex repetidamente, escribir re.search(r"...", text) una y otra vez hace que el motor parsee (compile) el patrón cada vez, lo cual es trabajo desperdiciado. `re.compile(patrón)` construye un objeto patrón compilado una vez, y llamas métodos sobre él como pattern.search(...) / pattern.findall(...) / pattern.sub(...). El código se lee mejor y corre más rápido.

Cómo se usa re.compile
r"\d{2,4}-\d{4}-\d{4}"re.compile(...)phone_re(objeto patrón)phone_re.search(text)phone_re.findall(text)phone_re.sub("*", text)
`re.compile(patrón)` te da un objeto patrón sobre el que puedes llamar .search / .findall / .sub tantas veces como necesites. Compila cuando reutilices el mismo patrón.
import re

# Reutilizar el mismo patrón de número de teléfono
phone_re = re.compile(r"\d{2,4}-\d{4}-\d{4}")

print(phone_re.findall("03-1234-5678 o 080-1111-2222"))
# ['03-1234-5678', '080-1111-2222']

print(phone_re.search("mi teléfono es 03-9999-0000").group())
# 03-9999-0000

print(phone_re.sub("<phone>", "Contacto: 03-1234-5678"))
# Contacto: <phone>

Construye el patrón de número de teléfono una vez con `re.compile`, luego cuenta y sustituye contra el mismo texto en una fila.

① Importa re.

② Define text = "Contacto: 03-1234-5678 o 090-9999-8888".

③ Compila un patrón de número de teléfono que sea 2-4 dígitos + 4 dígitos + 4 dígitos con re.compile, y guárdalo como phone_re.

④ Usa phone_re.findall(text) para contar cuántos números de teléfono hay, e imprímelo como phone count: ◯.

⑤ Usa phone_re.sub para reemplazar cada número de teléfono entero con `<phone>`, e imprime el resultado como replaced: ◯◯.

Editor Python

Ejecutar el código para ver el resultado
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1¿Qué devuelve re.match(r"\d+", "abc 123")?

Pregunta 2¿Cuál es el regex correcto para uno o más dígitos en una fila?

Pregunta 3De re.search(r"(\w+)@(\w+)", "alice@example"), ¿qué llamada devuelve solo el dominio?

Pregunta 4¿Cuál es la razón principal para usar una raw string `r"..."` al escribir regex en Python?