Expresiones regulares a la vista
·
Resumen del curso
1
El pago desaparecido: buscar el texto tal cual
2
El coche que se dio a la fuga: clases de caracteres
3
La guía telefónica rota: cuantificadores
4
Los registros falsos: anclas y validación
5
Fechas por todas partes: grupos y alternativas
6
El acceso de las 3 de la madrugada: grupos con nombre y objetos Match
7
La víspera de la noticia: sustitución y división
8
El desafío del Backtracker: codicioso, perezoso y posesivo
9
Las reglas de la caja fuerte: lookaround
10
El mismo modus operandi: retrorreferencias y condicionales
11
La regex ilegible: flags y compilación
12
El Backtracker, desenmascarado: repaso y un tokenizador
Tiempo 5:00–5:50
CASE-007
Grep & Co. Agencia de Detectives de Datos
Expediente
Cliente
El periodista Oh, que investiga una filtración
Pruebas
Una lista de clientes enviada por una fuente anónima. Parece el registro de socios de Mercado Jeong. Cada línea usa separadores distintos.
Para cerrar el caso
Dejarla lista para el artículo de mañana: tapar el centro de los teléfonos, pasar las fechas al formato 2026-10-20 y separar cada línea en columnas.
Las nueve de la noche. La puerta de la agencia se abre de golpe. El periodista Oh entra jadeando y abre su laptop. «Me ha llegado un soplo. Es la lista de clientes de Mercado Jeong. El artículo sale mañana a primera hora, pero no puedo publicar datos personales tal cual».
La directora se inclina sobre la pantalla. Es la misma lista de socios de las 3 de la madrugada que encontraste en la lección 6. «La búsqueda ya terminó. Hoy toca cambiar y cortar».
id, name; phone | joined
1001,kim,010-0000-5678,20/10/2026
1002; lee; 010-0000-1234; 19/10/2026
1003 | park | 010-0000-9012 | 03/09/2026
1004, choi;010-0000-3456 |28/02/2026
# (a+)+$En la última línea hay unos caracteres raros, pero el periodista Oh va contra reloj y no les presta atención.
re.sub(patrón, reemplazo, original) devuelve una cadena nueva en la que todas las coincidencias del patrón se han cambiado. El original no se toca: en Python las cadenas son inmutables.
import re
re.sub(r"\d{4}", "****", "010-0000-5678") # '010-****-****'Hay dos bloques de cuatro cifras, así que también se tapó el final. Para dejar visibles las tres primeras y las cuatro últimas cifras y tapar solo el centro, captura en grupos lo que quieres conservar y vuelve a llamarlo desde el reemplazo. \1 es el texto que capturó el grupo 1.
re.sub(r"(\d{3})-\d{4}-(\d{4})", r"\1-****-\2", "010-0000-5678") # '010-****-5678'El reemplazo también lleva barras invertidas, así que escríbelo como el patrón: con r"...".
Laboratorio de pruebas
re.search(r"", text)
Valor devuelto
Las fechas de la lista van como 20/10/2026, es decir, día/mes/año. En el artículo tienen que salir como 2026-10-20. Basta con recolocar los grupos.
re.sub(r"(\d\d)/(\d\d)/(\d{4})", r"\3-\2-\1", "20/10/2026") # '2026-10-20'
re.sub(r"(\d\d)/(\d\d)/(\d{4})", r"\g<3>-\g<2>-\g<1>", "20/10/2026") # '2026-10-20'\3 y \g<3> significan lo mismo. Los grupos con nombre de la lección 6 se llaman con \g<nombre>. Así no tienes que recordar el orden y te equivocas menos.
re.sub(r"(?P<d>\d\d)/(?P<m>\d\d)/(?P<y>\d{4})", r"\g<y>-\g<m>-\g<d>", "20/10/2026") # '2026-10-20'\g<0> es la coincidencia completa. Sirve para envolver lo encontrado sin borrarlo.
re.sub(r"\d{3}-\d{4}-\d{4}", r"[\g<0>]", "1001,kim,010-0000-5678") # '1001,kim,[010-0000-5678]'Cuidado si quieres poner una cifra justo después del número de grupo. \10 no es «el grupo 1 seguido de un 0», sino el grupo 10. En ese caso cierra el número con ángulos: \g<1>0.
re.sub(r"(\d+)", r"\g<1>0", "kim 5") # 'kim 50'
re.sub(r"(\d+)", r"\10", "kim 5") # re.PatternError: invalid group reference 10 at position 1En el reemplazo, \n también se convierte en un salto de línea. En cambio, un escape de letra sin significado, como \q, es un error.
re.sub(r",", r"\n", "a,b") # 'a\nb'
re.sub(r",", r"\q", "a,b") # re.PatternError: bad escape \q at position 0re.PatternError es el nombre que se usa desde Python 3.13; es la misma excepción que el antiguo re.error. Lo veremos con calma en la lección 12.
Laboratorio de pruebas
re.search(r"", text)
Valor devuelto
En lugar del texto de reemplazo puedes pasar una función. Cada vez que hay una coincidencia, re.sub le pasa el objeto Match a la función y usa la cadena que esta devuelva. Probemos a dejar solo la inicial del nombre y poner tantos asteriscos como letras falten. Eso no se puede hacer con un reemplazo fijo.
def hide(m):
name = m.group(1)
return "," + name[0] + "*" * (len(name) - 1) + ","
re.sub(r",([a-z]+),", hide, "1003,park,010-0000-9012,03/09/2026") # '1003,p***,010-0000-9012,03/09/2026'
re.sub(r"[a-z]+", lambda m: m[0].upper(), "kim and lee") # 'KIM AND LEE'TypeError.count indica cuántas coincidencias se reemplazan, empezando por el principio. El valor por defecto, 0, significa «todas». re.subn devuelve una tupla con la cadena resultante y el número de reemplazos. Supongamos que la lista de arriba está guardada en una cadena llamada leak.
re.sub(r"\d{3}-\d{4}-\d{4}", "***", leak, count=1).splitlines()[1:3]
# ['1001,kim,***,20/10/2026', '1002; lee; 010-0000-1234; 19/10/2026']
re.subn(r"\d{3}-\d{4}-\d{4}", "***", leak)[1] # 4Así el artículo puede decir con exactitud: «se taparon 4 números de teléfono».
Cada línea de la lista usa un separador distinto: comas, punto y coma, barras verticales, y espacios a un lado y a otro sin ningún orden. str.split solo acepta un separador. re.split corta por un patrón.
re.split(r"\s*[,;|]\s*", "a, b;c |d") # ['a', 'b', 'c', 'd']
re.split(r"\s*[,;|]\s*", "a, b;c |d", maxsplit=2) # ['a', 'b', 'c |d']\s* a cada lado del separador se traga también los espacios, así que las columnas salen limpias.maxsplit=2 corta solo dos veces y deja el resto tal cual en la última columna.Si el patrón tiene grupos de captura, los separadores cortados también se insertan en la lista resultante. Es útil cuando necesitas saber qué separador había. Si no lo necesitas, usa (?:...).
re.split(r"\s*([,;|])\s*", "a, b;c |d") # ['a', ',', 'b', ';', 'c', '|', 'd']También se puede cortar con coincidencias vacías, que no ocupan ningún carácter. \b solo coincide en los límites de palabra, así que corta entre cada palabra y cada espacio. Como también hay límites en los dos extremos, aparecen cadenas vacías.
re.split(r"\b", "a b") # ['', 'a', ' ', 'b', '']Laboratorio de pruebas
re.search(r"", text)
Valor devuelto
El cuarto argumento posicional de re.sub es count. Si, queriendo pasar un flag, escribes re.I en cuarta posición sin pensarlo, no entra como flag sino como número de reemplazos, porque el valor real de re.I es 2.
re.sub("a", "b", "aAaA", re.I) # 'bAbA' ← no ignora mayúsculas y solo cambia dos veces
re.sub("a", "b", "aAaA", flags=re.I) # 'bbbb'Este error es tan común que, desde Python 3.13, pasar count, maxsplit o flags como argumentos posicionales lanza un DeprecationWarning. La primera línea de arriba avisa con 'count' is passed as positional argument. Lo mismo vale para maxsplit y flags de re.split. Escríbelos siempre con nombre: count=1, maxsplit=2, flags=re.I.
El consejo de Dot
Si me dices «siéntate», me siento. Pero si solo gritas «¡dos!», no tengo ni idea de qué quieres. A las funciones les pasa igual: re.sub("a", "b", s, re.I) lo entiende como «cambia solo dos veces». Llámalo por su nombre: flags=re.I.
Encadenamos tapar, cambiar fechas y cortar. Dejamos fuera la cabecera y la última línea, y cortamos solo las filas de clientes.
masked = re.sub(r"(\d{3})-\d{4}-(\d{4})", r"\1-****-\2", leak)
fixed = re.sub(r"(?P<d>\d\d)/(?P<m>\d\d)/(?P<y>\d{4})", r"\g<y>-\g<m>-\g<d>", masked)
for row in fixed.splitlines()[1:-1]:
print(re.split(r"\s*[,;|]\s*", row))
# ['1001', 'kim', '010-****-5678', '2026-10-20']
# ['1002', 'lee', '010-****-1234', '2026-10-19']
# ['1003', 'park', '010-****-9012', '2026-09-03']
# ['1004', 'choi', '010-****-3456', '2026-02-28']El periodista Oh recoge la tabla y se dispone a irse, pero la directora levanta la mano. «Un momento. La última línea».
print(fixed.splitlines()[-1]) # # (a+)+$No son datos de la lista. Es una expresión regular que alguien dejó ahí a propósito. Para buscar estos caracteres tal cual, hay que desactivar los metacaracteres uno por uno, como aprendiste en la lección 1.
print(re.escape("(a+)+$")) # \(a\+\)\+\$
re.search(r"\(a\+\)\+\$", leak) # <re.Match object; span=(177, 183), match='(a+)+$'>Laboratorio de pruebas
re.search(r"", text)
Valor devuelto
«¿Qué es esto?», pregunta el periodista Oh. La directora se queda callada un buen rato antes de contestar. «Una trampa de retroceso. Un motor de regex que lea esto vuelve sobre sus pasos una y otra vez hasta quedarse colgado. Quítalo del artículo de mañana».
Cuando el periodista se marcha, la directora saca del cajón un marco con una foto descolorida. Es del primer año de la agencia. La mira largo rato y luego deja el marco boca abajo sobre el escritorio.
Informe de caso cerrado — Se taparon los dígitos centrales de los 4 teléfonos de la lista filtrada, se unificaron las fechas al formato año-mes-día y se entregó el resultado al periodista. Al final de la lista se encontró una regex colocada a propósito: (a+)+$. Origen desconocido. Requiere investigación aparte.
1001~1004 para que lleven # delante, como #1001. Con \g<0> se hace en una línea.re.split(r"(\s*)([,;|])(\s*)", "a, b"). Con tres grupos, ¿cuántos elementos se cuelan entre las columnas?"price 1500 and 300". No olvides que la función debe devolver una cadena.La libreta de la directora
re.sub(patrón, reemplazo, original) devuelve una cadena nueva. El original no cambia.
En el reemplazo, \1 = \g<1>, por nombre \g<name>, la coincidencia entera \g<0>. Si sigue una cifra, \g<1>0 (\10 es el grupo 10).
En el reemplazo, \n es un salto de línea; un escape desconocido como \q es un error.
Reemplazo con función: recibe un Match y devuelve una cadena. count limita los cambios, re.subn devuelve (resultado, cantidad).
re.split corta por un patrón. Tiene maxsplit, y si hay grupos de captura los separadores también entran en el resultado.
count, maxsplit y flags, siempre con nombre. Posicionales dan DeprecationWarning desde 3.13, y re.I acaba entrando como count=2.
← Anterior
Lección 6 · El acceso de las 3 de la madrugada: grupos con nombre y objetos Match
Siguiente →
Lección 8 · El desafío del Backtracker: codicioso, perezoso y posesivo
Recibe avisos de nuevas lecciones
Te avisaremos por correo cuando este curso tenga lecciones nuevas o se abra un curso de continuación.
R
ReflexKR
Aprende Python y Reflex haciendo. Todos los ejemplos se ejecutaron y verificaron en Reflex 0.10.0.
ReflexKR es gratis y sin banners publicitarios. Si te ayudó, puedes apoyarlo con un café. ☕ Invítanos un café
Aviso de afiliados© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Preguntas y respuestas
Pregunta lo que quieras sobre esta lección. Aquí se reúnen las preguntas de todas las versiones de idioma de esta página.