Expresiones regulares en Python

📅 Actualizado en marzo 2026 📊 Nivel: Intermedio ⏱️ 15 min de lectura

Las expresiones regulares son el bisturí del procesamiento de texto: precisas, potentes y un poco intimidantes la primera vez que las ves. Pero una vez que entiendes la lógica detrás de r'\b[\w.+-]+@[\w-]+\.[a-z]{2,6}\b', no puedes imaginar cómo vivías sin ellas. Extraer emails de un log, validar teléfonos, limpiar texto con espacios irregulares, encontrar fechas en cualquier formato: todo eso son tres líneas con el módulo re.

Medallón de Marco Fabio Quintiliano, retórico romano
Verbis omnibus adhibenda est cura
«A todas las palabras hay que prestarles cuidado»
Marco Fabio Quintiliano · Retórico romano · 35 d.C. – 96 d.C.
Quintiliano dedicó su vida a enseñar que el poder del lenguaje reside en la precisión: cada palabra en su sitio, con su peso exacto. Las expresiones regulares son la encarnación técnica de ese principio. En un patrón regex, cada carácter cuenta: un punto no es lo mismo que \., un * no es lo mismo que +, y una \b en el lugar equivocado puede convertir una validación correcta en un desastre silencioso. Cuida cada carácter del patrón como Quintiliano cuidaba cada palabra del discurso.

La búsqueda con superpoderes

Cuando Python tiene los métodos str.find(), str.replace() y str.split(), ¿para qué necesitas expresiones regulares? La respuesta es que esos métodos trabajan con texto literal: saben buscar exactamente la cadena "2026", pero no saben buscar "cualquier secuencia de cuatro dígitos". Las regex sí.

Una expresión regular es un patrón que describe un conjunto de strings. En lugar de decir "busca exactamente esto", dices "busca cualquier cosa que tenga esta estructura". El módulo re de Python es la librería estándar que implementa el motor de regex; no necesitas instalar nada.

Casos de uso cotidianos donde las regex son la herramienta correcta:

  • Validación de formatos: ¿Es esto un email válido? ¿Un teléfono español? ¿Un código postal?
  • Extracción de datos: Dame todas las fechas que aparecen en este documento. Dame todos los importes en euros de este PDF convertido a texto.
  • Limpieza de texto: Elimina todos los espacios dobles. Quita las etiquetas HTML. Normaliza los saltos de línea.
  • Parseo de logs: Extrae el timestamp, el nivel de log y el mensaje de cada línea de un fichero de logs con formato fijo.
Lupa de mano sobre páginas de texto impreso, ampliando los caracteres
Una lupa amplía y revela lo que estaba ahí pero pasaba desapercibido. re.search() hace exactamente eso: recorre el texto completo y te señala exactamente dónde está lo que buscas, junto con la posición y el texto capturado. Sin lupa puedes leer el texto; con lupa encuentras el detalle que necesitas. Fuente: Pexels (licencia libre).

El primer contacto con el módulo re es siempre el mismo: importar y probar una búsqueda básica.

import re

texto = "El pedido 42837 llegará el 2026-03-15 a las 10:30"

# Buscar el número de pedido (secuencia de dígitos)
m = re.search(r'\d+', texto)
if m:
    print(m.group())   # → 42837
    print(m.start())   # → 10  (posición donde empieza)
    print(m.end())     # → 15  (posición donde termina)

El r'' antes de la cadena indica un raw string: le dice a Python que no interprete los backslashes, dejando que el motor de regex los procese. Acostúmbrate a escribir siempre r'' en los patrones. Más adelante verás por qué es importante.

Metacaracteres esenciales

Los metacaracteres son los caracteres que tienen significado especial dentro de un patrón. Hay que memorizar un puñado de ellos para poder leer y escribir cualquier regex. El resto se puede consultar.

Clases de caracteres y cuantificadores

import re

# . → cualquier carácter excepto salto de línea
re.search(r'h.la', 'hola')       # coincide: . = 'o'
re.search(r'h.la', 'hXla')       # coincide: . = 'X'

# \d → dígito [0-9]   \D → no dígito
re.findall(r'\d+', 'abc 123 def 456')   # → ['123', '456']

# \w → alfanumérico + guión bajo   \W → lo contrario
re.findall(r'\w+', 'hola mundo!')       # → ['hola', 'mundo']

# \s → espacio/tab/\n   \S → no espacio
re.sub(r'\s+', ' ', '  demasiado   espacio  ')   # → '  demasiado espacio  '

# \b → límite de palabra (posición, no carácter)
re.findall(r'\bpython\b', 'python pythonista')    # → ['python']  (no 'pythonista')

Anclas: ^ y $

# ^ → inicio del string  $ → fin del string
re.match(r'^\d{4}', '2026-03-07')         # → coincide (empieza con 4 dígitos)
re.search(r'\d{2}$', 'Módulo 06')         # → coincide (termina en 2 dígitos)
re.fullmatch(r'\d{5}', '28080')           # → coincide (exactamente 5 dígitos)
re.fullmatch(r'\d{5}', '28080-Madrid')    # → None (hay más texto)

Clases personalizadas con []

# [abc] → a, b o c (exactamente uno de los listados)
# [a-z] → rango: cualquier letra minúscula
# [^abc] → cualquier cosa EXCEPTO a, b o c (negación)
# [a-zA-Z0-9_] → equivale a \w

re.findall(r'[aeiou]', 'python')        # → ['o']  (solo vocales)
re.findall(r'[^aeiou\s]+', 'hola py')  # → ['hl', 'py']  (consonantes juntas)
re.findall(r'[A-Z][a-z]+', 'Ana y Luis van a Madrid')  # → ['Ana', 'Luis', 'Madrid']

Cuantificadores: *, +, ?, {n,m}

# * → 0 o más   + → 1 o más   ? → 0 o 1
# {n} → exactamente n   {n,m} → entre n y m

re.findall(r'\d{2,4}', '12 345 6789 1')    # → ['12', '345', '6789']
re.search(r'colou?r', 'color')              # → coincide: u es opcional
re.search(r'colou?r', 'colour')             # → también coincide

# Lazy vs greedy: añade ? para capturar lo mínimo posible
texto_html = 'negrita y cursiva'
re.findall(r'<.+>',  texto_html)    # greedy:  ['<b>negrita</b> y <i>cursiva</i>']
re.findall(r'<.+?>', texto_html)    # lazy:    ['<b>', '</b>', '<i>', '</i>']
⚠️ El punto no incluye saltos de línea por defecto. r'.*' no cruza líneas. Si tu texto tiene múltiples líneas y necesitas que . coincida también con \n, añade el flag re.DOTALL.
Diagrama del motor de expresiones regulares en Python: anatomía de un patrón, las cuatro funciones principales y grupos de captura con ejemplos
El motor regex en tres niveles: cómo se lee un patrón (cada componente coloreado por función), qué devuelve cada función principal, y cómo los grupos de captura extraen partes específicas del match. Infografía: Ciberaula.

search, match, findall: elegir la correcta

El módulo re tiene cuatro funciones de búsqueda. Confundirlas es el error más frecuente entre los que aprenden regex en Python.

re.search() — la más usada

import re

texto = "Factura núm. 2026-0042 emitida el 07/03/2026"

m = re.search(r'\d{4}-\d{4}', texto)
if m:
    print(m.group())    # → '2026-0042'
    print(m.span())     # → (13, 22)  — posición inicio, fin
else:
    print("No encontrado")

re.search() recorre todo el string y devuelve un Match object con la primera coincidencia, o None si no hay ninguna. Comprueba siempre el resultado antes de llamar a .group(): si no hay match y llamas a m.group(), obtienes un AttributeError porque None no tiene ese método.

re.match() — solo al inicio

# re.match() solo coincide si el patrón está AL INICIO del string
re.match(r'\d+', '123abc')   # → Match: '123'
re.match(r'\d+', 'abc123')   # → None  (123 no está al principio)

# Equivalente a buscar con ^ al inicio:
re.search(r'^\d+', 'abc123')  # → None  (igual resultado)

re.fullmatch() — el string completo

# re.fullmatch() exige que el patrón coincida con TODO el string
# Perfecto para validaciones donde no puede haber nada extra

PATRON_CP = re.compile(r'[0-5]\d{4}')

re.fullmatch(r'[0-5]\d{4}', '28080')           # → Match ✓
re.fullmatch(r'[0-5]\d{4}', '28080-Madrid')    # → None  ✗ (sobra texto)
re.fullmatch(r'[0-5]\d{4}', '9876')            # → None  ✗ (faltan dígitos)

re.findall() — todos los matches a la vez

log = """
2026-03-07 10:14:22 ERROR conexión rechazada por 192.168.1.5
2026-03-07 10:15:01 INFO  petición recibida desde 10.0.0.1
2026-03-07 10:15:43 ERROR timeout al conectar con 192.168.1.5
"""

# Extraer todas las IPs del log
ips = re.findall(r'\b(?:\d{1,3}\.){3}\d{1,3}\b', log)
print(ips)    # → ['192.168.1.5', '10.0.0.1', '192.168.1.5']

# Extraer todas las horas
horas = re.findall(r'\d{2}:\d{2}:\d{2}', log)
print(horas)  # → ['10:14:22', '10:15:01', '10:15:43']

Cuando el patrón no tiene grupos, findall devuelve una lista de strings. Cuando tiene un grupo, devuelve la lista del contenido de ese grupo. Cuando tiene dos o más grupos, devuelve una lista de tuplas.

# Sin grupos → lista de matches completos
re.findall(r'\d+', 'a1 b22 c3')         # → ['1', '22', '3']

# Con un grupo → lista del contenido del grupo
re.findall(r'(\d+)', 'a1 b22 c3')       # → ['1', '22', '3']  (idéntico)

# Con dos grupos → lista de tuplas
re.findall(r'(\w+)=(\d+)', 'a=1 b=22')  # → [('a', '1'), ('b', '22')]

re.finditer() — cuando necesitas posición o muchos matches

# finditer devuelve un iterador de Match objects (más eficiente con textos grandes)
for m in re.finditer(r'\d+', 'página 12 sección 3 ítem 456'):
    print(f"'{m.group()}' en posición {m.start()}-{m.end()}")
# → '12'  en posición 7-9
# → '3'   en posición 19-20
# → '456' en posición 26-29

Grupos de captura y grupos nombrados

Los grupos son la característica más poderosa de las regex: permiten extraer partes específicas del match en lugar de solo saber si hay coincidencia.

Grupos numerados con ()

import re

# Extraer partes de una fecha
m = re.search(r'(\d{4})-(\d{2})-(\d{2})', '2026-03-07')
if m:
    print(m.group())    # → '2026-03-07'  (match completo)
    print(m.group(1))   # → '2026'  (grupo 1)
    print(m.group(2))   # → '03'    (grupo 2)
    print(m.group(3))   # → '07'    (grupo 3)
    print(m.groups())   # → ('2026', '03', '07')  (todos los grupos)

Grupos nombrados con (?P<nombre>...)

Los grupos nombrados son mejores que los numerados para todo lo que no sea trivial. Si cambias el patrón y añades un grupo antes, los índices de todos los grupos posteriores cambian. Con nombres, el código sigue funcionando.

patron_fecha = re.compile(
    r'(?P<anio>\d{4})-(?P<mes>\d{2})-(?P<dia>\d{2})'
)

m = patron_fecha.search('Entrega: 2026-03-15 antes del mediodía')
if m:
    print(m.group('anio'))    # → '2026'
    print(m.group('mes'))     # → '03'
    print(m.groupdict())      # → {'anio': '2026', 'mes': '03', 'dia': '15'}

# Con findall: devuelve lista de groupdict cuando hay grupos nombrados? No exactamente:
# findall devuelve tuplas aunque sean grupos nombrados; usa finditer para groupdict
for m in patron_fecha.finditer('2026-01-01 hasta 2026-12-31'):
    print(m.groupdict())
# → {'anio': '2026', 'mes': '01', 'dia': '01'}
# → {'anio': '2026', 'mes': '12', 'dia': '31'}

Grupos no capturantes con (?:...)

A veces necesitas agrupar parte del patrón (para aplicar un cuantificador al grupo) sin que ese grupo aparezca en el resultado:

# Con (?:...) el grupo se usa para estructura, no para captura
re.findall(r'(?:\d{1,3}\.){3}\d{1,3}', '192.168.1.5 y 10.0.0.1')
# → ['192.168.1.5', '10.0.0.1']   ← el grupo (?:...) no aparece en el resultado
Cerradura de puerta moderna con mecanismo de cilindro visible
Los pines de una cerradura solo se alinean con la llave exacta: ni un pin de más, ni uno de menos. re.fullmatch() funciona exactamente igual — el texto completo debe encajar en el patrón, carácter por carácter, sin sobrar ni faltar nada. Por eso se usa para validar emails, teléfonos o códigos postales: o coincide del todo, o la puerta no abre. Fuente: Pexels (licencia libre).

Sustituir y limpiar texto con re.sub()

re.sub(patrón, sustitución, texto) es la versión regex de str.replace(). Encuentra todas las coincidencias del patrón y las reemplaza por el string de sustitución. Devuelve siempre un string nuevo (las strings son inmutables en Python).

Sustituciones básicas

import re

# Normalizar espacios
limpio = re.sub(r'\s+', ' ', '  demasiado   espacio  en   blanco  ').strip()
print(limpio)   # → 'demasiado espacio en blanco'

# Eliminar caracteres no alfanuméricos (limpiar strings para slugs)
slug = re.sub(r'[^a-z0-9-]', '', 'Expresiones Regulares en Python!'.lower().replace(' ', '-'))
print(slug)     # → 'expresiones-regulares-en-python'

# Eliminar etiquetas HTML simples
sin_html = re.sub(r'<[^>]+>', '', '<b>Texto <em>importante</em></b>')
print(sin_html) # → 'Texto importante'

# Limitar a N sustituciones con el argumento count
texto = 'a1 b2 c3 d4'
print(re.sub(r'\d', 'X', texto, count=2))  # → 'aX bX c3 d4'

Usar grupos en la sustitución con \1 y \g<nombre>

# Reformatear fechas de DD/MM/YYYY a YYYY-MM-DD
fechas = "Entregado: 07/03/2026. Vence: 15/04/2026."

resultado = re.sub(
    r'(\d{2})/(\d{2})/(\d{4})',
    r'\3-\2-\1',                # \1=dia \2=mes \3=anio → reordenamos
    fechas
)
print(resultado)
# → "Entregado: 2026-03-07. Vence: 2026-04-15."

# Con grupos nombrados: más legible
resultado2 = re.sub(
    r'(?P<dia>\d{2})/(?P<mes>\d{2})/(?P<anio>\d{4})',
    r'\g<anio>-\g<mes>-\g<dia>',
    fechas
)
print(resultado2)   # → mismo resultado, código más legible

Sustitución con función

Si la sustitución no es un string fijo sino que depende del match, pasa una función como segundo argumento:

# Convertir todos los importes en euros a dólares (tasa x1.08)
def euros_a_dolares(m):
    importe = float(m.group(1).replace(',', '.'))
    return f"${importe * 1.08:.2f}"

texto = "Precio: 29,99€. IVA: 6,30€. Total: 36,29€."
print(re.sub(r'([\d,]+)€', euros_a_dolares, texto))
# → "Precio: $32.39. IVA: $6.80. Total: $39.19."
💡 re.subn() para contar sustituciones: Si necesitas saber cuántas sustituciones se hicieron, usa re.subn() que devuelve una tupla (nuevo_texto, num_sustituciones). Útil para auditorías o para saber si el texto cambió.

Flags: IGNORECASE, MULTILINE, DOTALL

Los flags modifican el comportamiento del motor de regex. Se pasan como tercer argumento a las funciones, o en el segundo argumento de re.compile(). Se pueden combinar con |.

import re

# ── re.IGNORECASE (re.I) ───────────────────────────────────────
# No distingue mayúsculas de minúsculas
re.findall(r'python', 'Python y PYTHON y python', re.IGNORECASE)
# → ['Python', 'PYTHON', 'python']

# ── re.MULTILINE (re.M) ───────────────────────────────────────
# ^ y $ aplican a cada línea, no solo al principio/fin del string
texto = "primera línea\nsegunda línea\ntercera línea"
re.findall(r'^\w+', texto, re.MULTILINE)
# → ['primera', 'segunda', 'tercera']   ← sin re.M: solo ['primera']

# ── re.DOTALL (re.S) ──────────────────────────────────────────
# El punto . coincide también con \n
html = "<div>\n  contenido\n  multilinea\n</div>"
re.search(r'<div>.*</div>', html)              # → None  (. no cruza líneas)
re.search(r'<div>.*</div>', html, re.DOTALL)   # → Match ✓

# ── re.VERBOSE (re.X) ─────────────────────────────────────────
# Permite espacios y comentarios en el patrón (para patrones complejos)
patron_email = re.compile(r"""
    [\w.+-]+        # usuario (con puntos, +, -)
    @               # arroba literal
    [\w-]+          # dominio principal
    \.              # punto literal
    [a-z]{2,6}      # TLD: 2 a 6 letras
""", re.IGNORECASE | re.VERBOSE)

# Combinar flags con |
patron = re.compile(r'^\d+', re.MULTILINE | re.IGNORECASE)
💡 Flags inline dentro del patrón: Puedes incluir flags directamente al inicio del patrón sin pasarlos como argumento: r'(?i)python' equivale a usar re.IGNORECASE. Los flags disponibles son (?i), (?m), (?s), (?x). Útil cuando compilas el patrón como string en una config externa.

re.compile() y buenas prácticas

El módulo re tiene una caché interna que almacena los últimos patrones compilados. Si usas un patrón una sola vez, la diferencia es mínima. Pero si lo aplicas en un bucle de millones de iteraciones, o si quieres dar nombre a los patrones para que el código sea legible, re.compile() es la herramienta correcta.

import re

# Compilar al nivel de módulo (se compila una sola vez al importar)
PATRON_FECHA   = re.compile(r'(?P<anio>\d{4})-(?P<mes>\d{2})-(?P<dia>\d{2})')
PATRON_EMAIL   = re.compile(r'[\w.+-]+@[\w-]+\.[a-z]{2,6}', re.IGNORECASE)
PATRON_TELEFONO = re.compile(r'(\+34)?[6789]\d{8}')

# El objeto Pattern tiene los mismos métodos que las funciones del módulo
m = PATRON_FECHA.search('Fecha de entrega: 2026-03-15')
emails = PATRON_EMAIL.findall(texto_largo)
limpio = re.sub(r'\s+', ' ', texto)   # para uso único, la función directa está bien

re.escape() — incluir texto literal en un patrón

Si necesitas buscar un string que puede contener metacaracteres (como un precio con punto, una URL con ?, una ruta de fichero), usa re.escape() para escaparlo automáticamente:

precio = "29.99€"    # el punto es metacarácter en regex
patron_literal = re.escape(precio)
print(patron_literal)         # → '29\\.99€'  (punto escapado)

# Útil cuando el patrón viene de input del usuario:
def buscar_literal(texto, buscar):
    return re.findall(re.escape(buscar), texto)

Las cinco reglas de oro

  1. Usa siempre raw strings r'' para los patrones. Sin excepción.
  2. Comprueba siempre el resultado de search/match antes de llamar a .group().
  3. Usa re.compile() para patrones que se usan más de una vez o que merecen un nombre descriptivo.
  4. Usa grupos nombrados (?P<nombre>...) en patrones con más de un grupo.
  5. No uses regex para todo. Para HTML usa BeautifulSoup. Para JSON usa el módulo json. Para splits simples usa str.split().
Ficha de referencia rápida del módulo re de Python: metacaracteres, funciones, grupos, flags y patrones comunes como email, teléfono y fecha
Referencia completa del módulo re: metacaracteres con ejemplos, las funciones y sus diferencias, grupos y flags, y patrones del mundo real listos para copiar. Ficha de referencia: Ciberaula.

Patrones del mundo real

Aquí la teoría aplicada a los casos que más vas a necesitar. Estos patrones están calibrados para ser útiles en Python sin caer en el error de intentar ser perfectos (un validador de email RFC 5322 completo ocupa páginas; estos son los que funcionan bien en el 99% de los casos reales).

import re

# ── Email (uso práctico, no RFC 5322 estricto) ────────────────
EMAIL = re.compile(r'[\w.+-]+@[\w-]+(?:\.[\w-]+)*\.[a-z]{2,10}', re.I)
emails = EMAIL.findall("Escribe a soporte@ciberaula.com o info@ciberaula.es")
# → ['soporte@ciberaula.com', 'info@ciberaula.es']

# Validar (string completo = email):
def es_email(s): return bool(EMAIL.fullmatch(s))

# ── Teléfono España (móvil + fijo, con o sin +34) ─────────────
TELEFONO_ES = re.compile(r'(\+34\s?|0034\s?)?[6789]\d{2}[\s.-]?\d{3}[\s.-]?\d{3}')
print(TELEFONO_ES.findall("Llama al 612 345 678 o al +34 91 234 5678"))

# ── Fecha DD/MM/YYYY → convertir a YYYY-MM-DD ─────────────────
FECHA_DMY = re.compile(r'(?P<d>\d{2})/(?P<m>\d{2})/(?P<y>\d{4})')
normalizada = FECHA_DMY.sub(r'\g<y>-\g<m>-\g<d>', "Entrega: 07/03/2026")
print(normalizada)   # → "Entrega: 2026-03-07"

# ── Extraer importes en euros ─────────────────────────────────
IMPORTE = re.compile(r'(\d{1,3}(?:\.\d{3})*(?:,\d{2})?)[\s]*€')
print(IMPORTE.findall("Total: 1.234,56 € + IVA 258,86 €"))
# → ['1.234,56', '258,86']

# ── Parsear línea de log ──────────────────────────────────────
LOG_LINE = re.compile(
    r'(?P<fecha>\d{4}-\d{2}-\d{2})\s'
    r'(?P<hora>\d{2}:\d{2}:\d{2})\s'
    r'(?P<nivel>DEBUG|INFO|WARNING|ERROR|CRITICAL)\s+'
    r'(?P<mensaje>.+)'
)
linea = "2026-03-07 10:14:22 ERROR conexión rechazada por 192.168.1.5"
m = LOG_LINE.match(linea)
if m:
    print(m.groupdict())
    # → {'fecha': '2026-03-07', 'hora': '10:14:22', 'nivel': 'ERROR', 'mensaje': 'conexión rechazada...'}

# ── Limpiar texto para slugs URL ──────────────────────────────
def to_slug(texto):
    texto = texto.lower()
    texto = re.sub(r'[áàäâ]', 'a', texto)
    texto = re.sub(r'[éèëê]', 'e', texto)
    texto = re.sub(r'[íìïî]', 'i', texto)
    texto = re.sub(r'[óòöô]', 'o', texto)
    texto = re.sub(r'[úùüû]', 'u', texto)
    texto = re.sub(r'ñ', 'n', texto)
    texto = re.sub(r'[^a-z0-9]+', '-', texto)
    return texto.strip('-')

print(to_slug("Expresiones Regulares: el poder oculto"))
# → "expresiones-regulares-el-poder-oculto"
💡 regex101.com: Antes de poner un patrón en producción, pruébalo en regex101.com seleccionando "Python". Muestra en tiempo real qué partes del patrón coinciden con qué partes del texto, explica cada componente del patrón y te avisa de errores. Es la herramienta de depuración número uno para regex.

❓ Preguntas frecuentes

❓ Preguntas frecuentes sobre Expresiones regulares en Python

Las dudas más comunes respondidas de forma clara y directa.

re.match() solo busca al principio del string: si el patrón no coincide con los primeros caracteres, devuelve None aunque aparezca más adelante. re.search() recorre todo el string buscando la primera coincidencia en cualquier posición. En la práctica, re.search() es mucho más utilizado porque el texto rara vez empieza exactamente por lo que buscas. Usa re.match() solo cuando necesites validar que el string empieza con un formato concreto, y re.fullmatch() cuando el string completo deba coincidir con el patrón, como al validar un email o un código postal.
Las expresiones regulares usan backslash como metacarácter especial: \d (dígito), \w (palabra), \b (límite de palabra). En un string normal de Python, el backslash también tiene significado especial: \n (salto de línea), \t (tabulador). Si escribes "\d" sin el prefijo r, Python lo interpreta primero como secuencia de escape; si no reconoce la secuencia, la deja como está, pero el comportamiento es indefinido. Con r"\d" le dices a Python que no interprete los backslashes, y el motor regex recibe exactamente la cadena \d. Usa siempre r"" para los patrones regex y evitarás bugs difíciles de detectar.
Si aplicas el mismo patrón una sola vez, la diferencia es irrelevante. Pero si aplicas el mismo patrón en un bucle de miles de iteraciones, re.compile() compila el patrón una sola vez y reutiliza el objeto Pattern resultante, lo que mejora el rendimiento. La otra razón para usar re.compile() es la legibilidad: puedes darle un nombre descriptivo al patrón (patron_email, patron_fecha) y documentar su propósito con re.VERBOSE. En producción, compila siempre los patrones que uses repetidamente a nivel de módulo (fuera de funciones) para que se compilen una sola vez al importar el módulo.
Los cuantificadores * y + son greedy (voraces) por defecto: intentan capturar el máximo texto posible que aún permita que el patrón completo coincida. Esto causa el problema clásico con HTML: r"<.+>" sobre "negrita y cursiva" captura todo desde el primer < hasta el último >, no cada etiqueta por separado. La solución es añadir ? después del cuantificador para hacerlo lazy (perezoso): r"<.+?>" captura solo hasta el primer >. Regla práctica: usa lazy cuando quieras capturar la ocurrencia más corta posible, greedy (por defecto) cuando quieras la más larga.
Las regex son una herramienta poderosa pero no universal. No uses regex para parsear HTML o XML: los documentos reales tienen anidamiento, comentarios y malformaciones que rompen cualquier patrón; usa BeautifulSoup o lxml. No uses regex para validar JSON; usa json.loads(). No uses regex cuando tienes métodos de string más simples: para comprobar si un string empieza por "http", str.startswith("http") es más legible y más rápido que re.match(r"http", s). Las regex brillan en extracción de datos de logs, limpieza de texto, validación de formatos simples y sustituciones complejas en las que str.replace() no es suficiente.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Expresiones regulares en Python? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!