R

ReflexKR

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

Tiempo 3:20–4:10

Fechas por todas partes: grupos y alternativas

CASE-005

Grep & Co. Agencia de Detectives de Datos

Expediente

Fechas por todas partes

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=wait

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

5-1. Agrupar con paréntesis: cuantificadores sobre grupos (5 min)

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.

5-2. Alternativas: |, esto o aquello (8 min)

| 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'>
  • Pon delante las alternativas largas.
  • Una alternativa entre caracteres sueltos, -|\.|/, 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

ship=done|wait es 'ship=done' o 'wait'. Encierra entre paréntesis solo la parte que se elige.

5-3. Sacar trozos por número de grupo (8 min)

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-20
  • group(0) (o simplemente group()) es la coincidencia entera. Los grupos empiezan en el 1.
  • groups() devuelve una tupla del grupo 1 hasta el último.
  • Si le pasas varios números, como en 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       4
m = 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

Encierra entre paréntesis el año, el mes y el día, cada uno por separado. Cada grupo se pinta de un color, y en la casilla de abajo aparecen m.group(1) a (3).

5-4. Paréntesis que no capturan: (?:...) (5 min)

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.

5-5. findall y los grupos (8 min)

Cuando el patrón tiene grupos de captura, findall cambia su comportamiento.

Grupos de capturaQué devuelve findall
0una lista con las coincidencias enteras
1una lista con las cadenas de ese grupo
2 o másuna 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

findall devuelve tuplas de separadores. Cambia los dos paréntesis por (?: o elige finditer para que salgan las fechas enteras.

5-6. Grupos que no coinciden y grupos repetidos (6 min)

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 (?:...).

Caso resuelto (10 min)

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

Este patrón de fecha solo acepta guiones. Cambia la posición de los separadores por [-./]? para que acepte las cuatro formas.

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.

Ejercicios

  1. Adivina el resultado de re.findall(r"ship=(done|wait)", orders). ¿Qué cambia si lo pasas a (?:done|wait)?
  2. ¿Qué devuelve groups() en re.search(r"(\d{4})-(\d\d)(-(\d\d))?", "2026-10 envío")?
  3. Escribe un patrón que saque con grupos la hora, los minutos y los segundos de 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.

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