R

ReflexKR

Documentación de Reflex
Expresiones regulares a la vista
Lección 7

Tiempo 5:00–5:50

La víspera de la noticia: sustitución y división

CASE-007

Grep & Co. Agencia de Detectives de Datos

Expediente

La víspera de la noticia

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.

7-1. Buscar y reemplazar: re.sub (8 min)

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

Ahora mismo también se convierten en asteriscos el número de socio y el año. Corrige el patrón para que solo coincida con los teléfonos y cambia el reemplazo a \1-****-\2 para tapar solo el centro. Si pulsas subn, verás además cuántos cambios se hicieron.

7-2. Grupos dentro del reemplazo: \g<1>, \g, \g<0> (10 min)

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 1

En 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 0

re.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

El reemplazo está en orden día-mes-año. Cámbialo a \g<y>-\g<m>-\g<d>. Prueba también qué pasa si pones \g<0> o \10.

7-3. Reemplazar con una función, y además count y subn (8 min)

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'
  • La función recibe un Match y tiene que devolver una cadena. Si devuelve un número, obtendrás un TypeError.
  • El Laboratorio de pruebas no puede ejecutar reemplazos con función. Pruébalos tú en Python.

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]   # 4

Así el artículo puede decir con exactitud: «se taparon 4 números de teléfono».

7-4. Cortar: re.split (9 min)

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']
  • El \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

Si cortas solo por comas, los punto y coma y las barras verticales se quedan dentro de las columnas. Usa como separador los tres signos junto con los espacios de alrededor, pero no cortes por el - de los teléfonos.

7-5. count, maxsplit y flags, siempre con nombre (5 min)

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.

Caso resuelto (10 min)

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

Si pones la firma tal cual como patrón, no encuentra los caracteres de la firma: es una regex que busca varias letras a seguidas, como el aaa de las condiciones del caso. Desactiva todos los metacaracteres para encontrar el texto de la firma en sí.

«¿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.

Ejercicios

  1. Cambia los números de socio 1001~1004 para que lleven # delante, como #1001. Con \g<0> se hace en una línea.
  2. Predice el resultado de re.split(r"(\s*)([,;|])(\s*)", "a, b"). Con tres grupos, ¿cuántos elementos se cuelan entre las columnas?
  3. Con un reemplazo por función, duplica todos los números de "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.

Datos: tu correo electrónico · Finalidad: avisos de nuevas lecciones o de la publicación de este curso · Conservación: hasta que pidas que lo borremos. No lo usamos para nada más ni lo compartimos. Responde a cualquier aviso y lo borraremos enseguida.

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.

Se muestra como texto plano, sin enlaces ni formato.

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
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex