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 3:20–4:10
CASE-005
Grep & Co. Agencia de Detectives de Datos
Expediente
Cliente
La señora Jeong, de la tienda en línea «Mercado Jeong»
Pruebas
El registro de pedidos de Mercado Jeong. Cada empleado a tiempo parcial escribió las fechas a su manera.
Para cerrar el caso
Sacar por separado el año, el mes y el día de todos los pedidos y dejarlos todos con el mismo formato.
Al día siguiente del caso de los registros falsos, la señora Jeong vuelve. Esta vez lo primero que hace es suspirar. «Los registros ya están bloqueados. Pero quería ordenar los pedidos por fecha, y resulta que mis tres empleados de media jornada escribieron las fechas cada uno a su manera». La directora da un sorbo al café. «A buscar ya aprendiste en la parte 1. Ahora toca aprender a sacar cosas de lo que encuentras».
El registro de pedidos es este.
#1001 2026-10-20 ship=done
#1002 2026.10.20 ship=wait
#1003 2026/10/19 ship=done
#1004 20261019 ship=done
#1005 2026-10/20 ship=waitNo basta con encontrar las fechas. Para ordenarlas hay que sacar «2026», «10» y «20» cada uno por su lado. La herramienta de hoy son los paréntesis, es decir, los grupos.
En la lección 3 vimos que un cuantificador solo afecta al bloque de justo delante. Si encierras varios caracteres entre paréntesis ( ), se convierten en un solo bloque.
re.fullmatch(r"ha+", "hahaha") # None
re.fullmatch(r"(ha)+", "hahaha") # <re.Match object; span=(0, 6), match='hahaha'>Formas como una dirección IP, donde «número y punto» se repite tres veces, también se escriben poniendo un cuantificador a un grupo.
re.search(r"(\d{1,3}\.){3}\d{1,3}", "ip=203.0.113.66")
# <re.Match object; span=(3, 15), match='203.0.113.66'>(\d{1,3}\.){3} es «un número de 1 a 3 dígitos y un punto», tres veces. Detrás del último número no hay punto, así que se añade \d{1,3} aparte.
| significa «lo de la izquierda o lo de la derecha». Pero en una regex, | tiene la prioridad más baja de todas. Si no la frenas con paréntesis, parte el patrón entero en dos.
re.fullmatch(r"ship=done|wait", "wait") # <re.Match object; span=(0, 4), match='wait'>
re.fullmatch(r"ship=(done|wait)", "wait") # None
re.fullmatch(r"ship=(done|wait)", "ship=wait") # <re.Match object; span=(0, 9), match='ship=wait'>ship=done|wait es «ship=done o wait». Si lo que quieres es «ship= seguido de done o wait», encierra entre paréntesis solo la parte que se elige.
Hay que tener especial cuidado al mezclarla con anclas. ^a|b$ no es «una cadena que solo tiene a o b», sino «^a o b$».
re.findall(r"^a|b$", "a.b") # ['a', 'b']
re.search(r"^ok|no$", "say no") # <re.Match object; span=(4, 6), match='no'>say no no empieza por ok y, aun así, coincide. Para lo que se pretendía, hay que escribir ^(ok|no)$.
Las alternativas se prueban de izquierda a derecha, y se usa la primera que funcione. No se elige la más larga.
re.search(r"10|100", "100") # <re.Match object; span=(0, 2), match='10'>
re.search(r"100|10", "100") # <re.Match object; span=(0, 3), match='100'>-|\.|/, es lo mismo que la clase [-./]. En estos casos, la clase es más corta y más rápida.Laboratorio de pruebas
re.search(r"", text)
Valor devuelto
Los paréntesis tienen un segundo papel, además de agrupar: recuerdan aparte el texto que coincidió dentro de ellos. Esto se llama grupo de captura, y se numeran 1, 2, 3… según el orden de los paréntesis de apertura.
Supongamos que el registro de pedidos de arriba está en una cadena llamada orders.
m = re.search(r"(\d{4})-(\d\d)-(\d\d)", orders)
print(m.group(0)) # 2026-10-20
print(m.group(1)) # 2026
print(m.group(2), m.group(3)) # 10 20
print(m.groups()) # ('2026', '10', '20')
print(m.group(1, 2)) # ('2026', '10')
print(m[1], m[0]) # 2026 2026-10-20group(0) (o simplemente group()) es la coincidencia entera. Los grupos empiezan en el 1.groups() devuelve una tupla del grupo 1 hasta el último.group(1, 2), devuelve una tupla.m[1] es una forma abreviada de m.group(1).Aunque los paréntesis estén anidados, la regla es la misma: se numeran por el orden en que aparece cada paréntesis de apertura (.
((\d{4})-(\d\d))-(\d\d)
12 3 4m = re.search(r"((\d{4})-(\d\d))-(\d\d)", "2026-10-20")
print(m.groups()) # ('2026-10', '2026', '10', '20')Laboratorio de pruebas
re.search(r"", text)
Valor devuelto
Muchas veces solo quieres agrupar, sin necesidad de recordar nada: cuando usas los paréntesis para delimitar un | o para ponerle un cuantificador a algo. Para eso está (?:...). Agrupa, pero no recibe número.
m = re.search(r"(?:\d{4})-(\d\d)", "2026-10")
print(m.group(1)) # 10
print(re.fullmatch(r"(?:ab)+", "abab").groups()) # ()Como el primer paréntesis no recibe número, (\d\d) pasa a ser el grupo 1. Si capturas solo lo que necesitas, la numeración queda más sencilla y, además, esquivas la trampa de findall del siguiente apartado.
Cuando el patrón tiene grupos de captura, findall cambia su comportamiento.
| Grupos de captura | Qué devuelve findall |
|---|---|
| 0 | una lista con las coincidencias enteras |
| 1 | una lista con las cadenas de ese grupo |
| 2 o más | una lista de tuplas con los grupos |
re.findall(r"\d{4}[-./]\d\d[-./]\d\d", orders)
# ['2026-10-20', '2026.10.20', '2026/10/19', '2026-10/20']
re.findall(r"(\d{4})[-./]\d\d[-./]\d\d", orders)
# ['2026', '2026', '2026', '2026']
re.findall(r"(\d{4})[-./](\d\d)[-./](\d\d)", orders)
# [('2026', '10', '20'), ('2026', '10', '20'), ('2026', '10', '19'), ('2026', '10', '20')]Si necesitas año, mes y día, la lista de tuplas viene de perlas. El problema es que los paréntesis que solo pusiste para agrupar también se convierten en grupos.
re.findall(r"\d{4}(-|\.|/)\d\d(-|\.|/)\d\d", orders)
# [('-', '-'), ('.', '.'), ('/', '/'), ('-', '/')]Querías las fechas y solo te han devuelto los separadores. Si necesitas la coincidencia entera, cambia esos paréntesis por (?:...) o usa finditer para recibir objetos Match y llamar a group().
re.findall(r"\d{4}(?:-|\.|/)\d\d(?:-|\.|/)\d\d", orders)
# ['2026-10-20', '2026.10.20', '2026/10/19', '2026-10/20']
[m.group() for m in re.finditer(r"\d{4}(-|\.|/)\d\d(-|\.|/)\d\d", orders)]
# ['2026-10-20', '2026.10.20', '2026/10/19', '2026-10/20']El consejo de Dot
Si findall te devuelve algo sospechosamente corto, o tuplas, lo primero es contar los paréntesis del patrón. Muchas veces unos paréntesis que solo eran para agrupar se han convertido en grupo a escondidas. Lo de la lección 3, cuando pasé (ha)+ por findall y solo salía 'ha', era justo por esto.
Laboratorio de pruebas
re.search(r"", text)
Valor devuelto
Si un grupo no participa en la coincidencia, su valor es None. Pasa cuando un grupo con ? coincidió cero veces, o cuando se eligió la otra rama de un |.
m = re.fullmatch(r"(\d{4})-(\d\d)(-(\d\d))?", "2026-10")
print(m.groups()) # ('2026', '10', None, None)
print(m.group(3)) # None
print(re.search(r"(a)|(b)", "b").groups()) # (None, 'b')Si le pones un cuantificador a un grupo y coincide varias veces, en el grupo solo queda la última vez.
m = re.fullmatch(r"(\d)+", "2026")
print(m.group(0), m.group(1)) # 2026 6
re.findall(r"(\d{1,3}\.){3}\d{1,3}", "a=203.0.113.66 b=198.51.100.7")
# ['113.', '100.']
re.findall(r"(?:\d{1,3}\.){3}\d{1,3}", "a=203.0.113.66 b=198.51.100.7")
# ['203.0.113.66', '198.51.100.7'](\d)+ coincidió cuatro veces, pero en el grupo 1 solo quedó el último 6. Con la IP pasa igual: en el grupo solo quedó la tercera repetición, 113., y findall devolvió ese grupo. Para usar el patrón de IP del apartado 5-1 con findall, la respuesta es (?:...).
Los tres separadores, -, . y /, se aceptan con [-./], y la fecha sin separadores, 20261019, con ?. El año, el mes y el día se capturan por separado.
El código imprime la fecha al estilo coreano: 년 es «año», 월 es «mes» y 일 es «día».
DATE = r"(\d{4})[-./]?(\d\d)[-./]?(\d\d)"
for m in re.finditer(DATE, orders):
y, mo, d = m.groups()
print(f"{y}년 {int(mo)}월 {int(d)}일")
# 2026년 10월 20일
# 2026년 10월 20일
# 2026년 10월 19일
# 2026년 10월 19일
# 2026년 10월 20일Para sacar también el número de pedido, se añade un grupo más. Como hay cuatro grupos, findall devuelve tuplas de cuatro elementos.
re.findall(r"#(\d+) " + DATE, orders)
# [('1001', '2026', '10', '20'), ('1002', '2026', '10', '20'), ('1003', '2026', '10', '19'), ('1004', '2026', '10', '19'), ('1005', '2026', '10', '20')]Laboratorio de pruebas
re.search(r"", text)
Valor devuelto
Informe de caso cerrado — Se ordenan las fechas de los 5 pedidos separando año, mes y día. Eso sí, el #1005 se introdujo con separadores mezclados y un empleado tiene que revisarlo.
Al recibir la tabla ya ordenada, la señora Jeong señala la última línea. «El #1005 es 2026-10/20. ¿Esto también ha pasado sin más?».
re.fullmatch(DATE, "2026-10/20")
# <re.Match object; span=(0, 10), match='2026-10/20'>Pues sí. Los dos [-./]? no saben nada el uno del otro. Con el patrón actual no hay forma de decir «si delante usaste -, detrás también tiene que ser -». La directora apunta una línea en su libreta. «Hay una manera de volver a llamar más adelante a lo que capturaste antes. Eso, en la lección 10». Por hoy, ese pedido se revisa a ojo y se da el día por terminado.
re.findall(r"ship=(done|wait)", orders). ¿Qué cambia si lo pasas a (?:done|wait)?groups() en re.search(r"(\d{4})-(\d\d)(-(\d\d))?", "2026-10 envío")?03:11:02, y comprueba qué devuelve m.group(1, 3).La libreta de la directora
( ) agrupa en un solo bloque y recuerda el texto que coincidió. Se le ponen cuantificadores, como en (ab)+.
| tiene la prioridad más baja. ^a|b$ es '^a o b$'. Las alternativas se prueban de izquierda a derecha.
Los grupos se numeran por el orden de los paréntesis de apertura. group(0) es todo; groups(), una tupla; m[1] es group(1).
(?:...) solo agrupa: no recibe número.
findall: sin grupos, la coincidencia entera; con 1, ese grupo; con 2 o más, tuplas. Si necesitas todo, (?:...) o finditer.
Un grupo que no coincide es None; un grupo repetido, como (\d)+, solo guarda la última vez.
← Anterior
Lección 4 · Los registros falsos: anclas y validación
Siguiente →
Lección 6 · El acceso de las 3 de la madrugada: grupos con nombre y objetos Match
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.