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
CASE-011
Grep & Co. Agencia de Detectives de Datos
Expediente
Cliente
La directora Han Byeol
Pruebas
La última línea de cada una de las tres cartas de amenaza y el registro de la puerta de la 4.ª planta de la agencia.
Para cerrar el caso
Unir las tres líneas en una sola regex, reescribirla de forma legible y averiguar qué busca.
Las once de la noche. En la agencia solo queda una luz encendida. La directora, sentada ante su viejo teclado mecánico, está copiando la última línea de cada carta. «Una regex escrita en una sola línea solo la entiende quien la escribió. Y a los tres meses, ni él».
Aquí tienes el archivo con las colas de las tres cartas, en orden, y el registro de la puerta de la 4.ª planta de la agencia, que la directora ha sacado sin decir nada. En los fragmentos de la regex se ven door, 4f y badge (puerta, 4.ª planta y tarjeta de acceso).
<<(?mi)^(\d{4})([-./])(\d\d)\2(\d\d)[ T]
(0[0-4]):(\d\d)\s+(?:door|gate)=4f\s+
badge=(f-0[12])\b(?!.*denied)>>2026-10-19 22:41 door=4F badge=F-01 result=ok
2026-10-20 02:13 door=4F badge=S-07 result=ok
2026-10-20 02:58 door=4F badge=F-02 result=denied
2026-10-20 03:02 door=4F badge=F-02 result=ok
2026.10.20 03:05 gate=4F badge=f-02 result=ok
2026-10-20 09:00 door=4F badge=F-01 result=okLa primera pista es el (?mi) del principio de la regex. Es un flag: un interruptor que, desde fuera del patrón, cambia la forma en que se lee el patrón. Hoy vamos a desenredar esta línea encendiendo los flags uno por uno.
En el registro de accesos, solo el lector de la puerta trasera (gate) anota la tarjeta en minúsculas: f-02. Con re.I (re.IGNORECASE) no se distinguen mayúsculas y minúsculas.
import re
re.findall(r"badge=F-02", door_log) # ['badge=F-02', 'badge=F-02']
re.findall(r"badge=F-02", door_log, re.I) # ['badge=F-02', 'badge=F-02', 'badge=f-02']re.M (re.MULTILINE) ya lo probaste un poco en la lección 4. Normalmente ^ solo coincide al principio de la cadena y $, al final. Con re.M, coinciden al principio y al final de cada línea. Aquí door_log son las seis líneas del registro de arriba.
re.findall(r"^\S+ 0[0-4]:\d\d", door_log) # []
re.findall(r"^\S+ 0[0-4]:\d\d", door_log, re.M)
# ['2026-10-20 02:13', '2026-10-20 02:58', '2026-10-20 03:02', '2026.10.20 03:05']
re.findall(r"\w+$", door_log) # ['ok']
re.findall(r"\w+$", door_log, re.M) # ['ok', 'ok', 'denied', 'ok', 'ok', 'ok']Sin re.M no sale ningún acceso de madrugada, porque la primera línea es de las 10 de la noche. Cuando trabajas con logs línea a línea, re.M es casi obligatorio.
Laboratorio de pruebas
re.search(r"", text)
Flags
Valor devuelto
Ahora hay que unir las tres líneas de las colas en una sola. Parece que bastaría con capturar desde << hasta >>, pero no sale nada.
m = re.search(r"<<(.*)>>", tails)
print(m) # NoneDot lo viene diciendo desde la lección 1: . no cruza los saltos de línea. El .* que sigue a << se detiene al final de la primera línea, y ahí no hay >>.
La directora gira la silla y abre la ventana. «Dot, sal». Con re.S (re.DOTALL), . pasa a ser cualquier carácter de verdad, saltos de línea incluidos.
m = re.search(r"<<(.*)>>", tails, re.S)
one_line = m.group(1).replace("\n", "")
print(one_line)
# (?mi)^(\d{4})([-./])(\d\d)\2(\d\d)[ T](0[0-4]):(\d\d)\s+(?:door|gate)=4f\s+badge=(f-0[12])\b(?!.*denied)Dot se apoya en el alféizar, salta fuera, vuelve a entrar por la ventana de al lado y se sienta en el escritorio. Las tres líneas ya son una.
El consejo de Dot
¡Han abierto la ventana! Con re.S puedo saltar los saltos de línea y llegar a la línea siguiente, y a la otra. Pero cuidado: .* es una glotona, y con la ventana abierta se va corriendo de un tirón hasta el final del texto. Si quieres capturar poco en varias líneas, usa .*?.
Laboratorio de pruebas
re.search(r"", text)
Flags
Valor devuelto
re.M y re.S tienen nombres parecidos y se confunden a menudo, pero hacen cosas completamente distintas.
re.M cambia dónde coinciden ^ y $ (en cada línea).re.S cambia qué caracteres puede comerse . (saltos de línea incluidos).re.findall(r"^.+$", "a\nb", re.M) # ['a', 'b']
re.findall(r"^.+$", "a\nb", re.S) # ['a\nb']Que la regex del Backtracker encienda solo (?mi) y no s tiene su motivo. El (?!.*denied) del final significa «en el resto de esta línea no puede aparecer denied». Con s encendido, .* saltaría a las líneas siguientes y descartaría el acceso si denied apareciera en cualquier línea de más abajo.
Ya es una sola línea, pero sigue sin leerse. Con re.X (re.VERBOSE), los espacios y saltos de línea del patrón se ignoran, y todo lo que va desde # hasta el final de la línea es un comentario. Así puedes repartir el patrón en varias líneas y explicarlo.
La directora lo vuelve a teclear trozo a trozo. Pone nombre a los grupos numerados y cambia \2 por el (?P=sep) de la lección 10.
DOOR = re.compile(r'''
^
(?P<year>\d{4}) (?P<sep>[-./]) (?P<month>\d\d) (?P=sep) (?P<day>\d\d) # fecha, con un solo tipo de separador
[ T] # entre fecha y hora: espacio o T
(?P<hour>0[0-4]) : (?P<minute>\d\d) # de las 0 a las 4 y pico de la madrugada
\s+ (?:door|gate) = 4f # puerta delantera o trasera de la 4.ª planta
\s+ badge = (?P<badge>f-0[12]) \b # tarjeta F-01 o F-02
(?! .* denied ) # fuera los accesos denegados
''', re.I | re.M | re.X)
for m in DOOR.finditer(door_log):
print(m["hour"], m["minute"], m["badge"])
# 03 02 F-02
# 03 05 f-02Es la misma regex, pero ahora se lee línea a línea. Buscaba los accesos en los que una tarjeta F-01 o F-02 entró de verdad por una puerta de la 4.ª planta entre las 0 y las 4 y pico de la madrugada.
re.X también tiene excepciones. Para buscar un espacio o un # de verdad, escápalos o mételos en una clase. Los espacios dentro de una clase no se eliminan; es el caso del [ T] del patrón de arriba.
re.fullmatch(r"\d{4} - \d\d", "2026-10", re.X) # <re.Match object; span=(0, 7), match='2026-10'>
re.fullmatch(r"a b", "a b", re.X) # None
re.fullmatch(r"a\ b", "a b", re.X) # <re.Match object; span=(0, 3), match='a b'>
re.fullmatch(r"a[ ]b", "a b", re.X) # <re.Match object; span=(0, 3), match='a b'>
re.fullmatch(r"a\#b", "a#b", re.X) # <re.Match object; span=(0, 3), match='a#b'>Si solo quieres meter un comentario corto en mitad del patrón sin usar re.X, usa (?#...). Todo lo que hay dentro del paréntesis se ignora. En el ejemplo, el comentario en coreano dice «tarjeta de los miembros fundadores».
re.findall(r"F-0[12](?#창립 멤버 출입증)", door_log, re.I)
# ['F-01', 'F-02', 'F-02', 'f-02', 'F-01']Laboratorio de pruebas
re.search(r"", text)
Flags
Valor devuelto
La directora se queda mirando largo rato el patrón desplegado. «Sin un solo nombre de variable, todo en una línea. Esa manía de poner comentarios… no, la manía de no ponerlos. Él también siempre…» Se interrumpe. Guarda en el cajón el viejo marco que había dejado boca abajo sobre el escritorio y vuelve a poner las manos en el teclado. «Sigamos».
En la lección 2 vimos que, en un patrón str, \w incluye el hangul y \d incluye el dígito arábigo ٣, porque Unicode es lo predeterminado. Con re.A (re.ASCII), \w \W \d \D \s \S \b \B solo tienen en cuenta caracteres ASCII.
re.findall(r"\w+", "café 한글 F_02") # ['café', '한글', 'F_02']
re.findall(r"\w+", "café 한글 F_02", re.A) # ['caf', 'F_02']
re.findall(r"\d", "٣3") # ['٣', '3']
re.findall(r"\d", "٣3", re.A) # ['3']re.U (re.UNICODE) es el valor por defecto de los patrones str, así que no hace falta escribirlo. Se mantiene por compatibilidad con código antiguo.re.L (re.LOCALE) solo se puede usar con patrones bytes y no se recomienda, porque el resultado cambia según la configuración regional de la computadora. Con un patrón str da error.re.compile(r"\w", re.L)
# ValueError: cannot use LOCALE flag with a str patternLaboratorio de pruebas
re.search(r"", text)
Flags
Valor devuelto
Los flags se combinan con |. Su valor es una enumeración llamada re.RegexFlag y se pueden tratar como enteros. También existe re.NOFLAG (desde la 3.11, valor 0), que significa «ningún flag» y va muy bien como valor por defecto de una función.
flags = re.I | re.M
print(flags) # re.IGNORECASE|re.MULTILINE
print(type(flags)) # <flag 'RegexFlag'>
print(int(flags)) # 10
def find_all(pattern, text, flags=re.NOFLAG):
return re.findall(pattern, text, flags)Como hace el Backtracker, también puedes escribir al principio del patrón las letras que necesites de (?aiLmsux). (?im) equivale a re.I | re.M. Si no va al principio, es un error.
re.findall(r"(?im)^\S+ 0\d:\d\d(?=.*f-02)", door_log)
# ['2026-10-20 02:58', '2026-10-20 03:02', '2026.10.20 03:05']
re.compile(r"badge=(?i)f-02")
# re.PatternError: global flags not at the start of the expression at position 6Para aplicar un flag solo a una parte del patrón, usa un rango entre paréntesis, como (?i:...). (?-i:...) lo apaga solo en ese rango, y (?i-s:...) enciende i y apaga s.
re.findall(r"badge=(?i:f)-02", door_log) # ['badge=F-02', 'badge=F-02', 'badge=f-02']
re.findall(r"(?i)BADGE=(?-i:F)-02", door_log) # ['badge=F-02', 'badge=F-02']
re.fullmatch(r"(?s)a.(?i-s:b.)", "a\nB\n") # None
re.fullmatch(r"(?s)a.(?i-s:b.)", "a\nBc") # <re.Match object; span=(0, 4), match='a\nBc'>a, L y u definen el alcance de los caracteres, así que dentro de un rango se pueden encender, pero no apagar.
re.findall(r"(?a:\w+)", "café") # ['caf']
re.compile(r"(?-a:\w+)")
# re.PatternError: bad inline flags: cannot turn off flags 'a', 'u' and 'L' at position 4Por último, re.DEBUG imprime tal cual cómo ha entendido re el patrón. Comparemos la firma del Backtracker con el mismo patrón sin la repetición exterior. Abajo está el principio de la salida; después sigue una lista de instrucciones de ejecución.
re.compile(r"(a+)+$", re.DEBUG)
# MAX_REPEAT 1 MAXREPEAT
# SUBPATTERN 1 0 0
# MAX_REPEAT 1 MAXREPEAT
# LITERAL 97
# AT AT_END
re.compile(r"a+$", re.DEBUG)
# MAX_REPEAT 1 MAXREPEAT
# LITERAL 97
# AT AT_ENDDentro de una repetición (MAX_REPEAT) hay otra repetición. Esa es exactamente la forma del patrón lento del desafío de la lección 8.
Igual que la directora guardó el patrón desplegado con el nombre DOOR, re.compile convierte un patrón en un objeto Pattern. Tiene los mismos métodos que las funciones del módulo, search match fullmatch findall finditer sub split, y además acepta un rango de búsqueda con pos y endpos.
p = re.compile(r"\d\d:\d\d")
line = "2026-10-20 03:02 door=4F"
print(p.search(line)) # <re.Match object; span=(11, 16), match='03:02'>
print(p.match(line)) # None
print(p.match(line, 11)) # <re.Match object; span=(11, 16), match='03:02'>
print(p.fullmatch(line, 11, 16)) # <re.Match object; span=(11, 16), match='03:02'>
print(p.findall(line, 0, 15)) # []
print(len(list(p.finditer(line, 11)))) # 1pos no es lo mismo que recortar la cadena. ^ no coincide en la posición pos, sino solo en el verdadero principio de la cadena (o de la línea, con re.M).
q = re.compile(r"^\d\d")
print(q.match(line, 11)) # None
print(q.match(line[11:])) # <re.Match object; span=(0, 2), match='03'>El objeto Pattern también guarda información sobre sí mismo. En un patrón str, flags incluye automáticamente re.UNICODE (32).
print(DOOR.groups) # 7
print(DOOR.groupindex)
# {'year': 1, 'sep': 2, 'month': 3, 'day': 4, 'hour': 5, 'minute': 6, 'badge': 7}
print(DOOR.flags) # 106
print(re.RegexFlag(DOOR.flags)) # re.IGNORECASE|re.UNICODE|re.MULTILINE|re.VERBOSE
print(re.compile("x").flags) # 32
print(re.compile(one_line).pattern == one_line) # Truere.search(patrón, ...), también compilan por dentro. Los patrones usados hace poco se quedan en una caché interna y no se vuelven a compilar. re.purge() vacía esa caché (rara vez hará falta).re.compile no es la velocidad, sino el nombre. Un patrón llamado DOOR se entiende al leer el código.re.Pattern[str] y re.Match[str] ([bytes] si el patrón es bytes).def night_entries(p: re.Pattern[str], log: str) -> list[re.Match[str]]:
return list(p.finditer(log))
print(len(night_entries(DOOR, door_log))) # 2Vuelves a leer el registro de accesos con el patrón desplegado.
for m in DOOR.finditer(door_log):
print(f"{m['year']}-{m['month']}-{m['day']} {m['hour']}:{m['minute']} {m['badge'].upper()}")
# 2026-10-20 03:02 F-02
# 2026-10-20 03:05 F-02La línea original sacada de las cartas encuentra exactamente las mismas dos líneas. Significan lo mismo; lo único que cambia es si se pueden leer.
[m.group(7) for m in re.finditer(one_line, door_log)] # ['F-02', 'f-02']La madrugada del 20 de octubre, la tarjeta F-02, rechazada a las 02:58 en la puerta delantera, volvió a pasar cuatro minutos después y entró; tres minutos más tarde cruzó la puerta trasera. El Backtracker envió esta regex para decir: «Si sabes leerla, sabrás cuándo vine».
La directora abre el registro de tarjetas. «La F-01 es la mía. La F-02…» En esa casilla, en lugar de un nombre, hay dos rayas tachándolo. «Tendría que haberla anulado hace diez años. No fui capaz».
Informe de caso cerrado — Las colas de las tres cartas formaban una sola regex. Una vez desplegada, se confirmó que la tarjeta F-02 accedió a la 4.ª planta de la agencia el 2026-10-20 entre las 03:02 y las 03:05. La F-02 es una tarjeta emitida cuando se fundó la agencia; queda desactivada desde hoy.
Por último, la directora deja la línea original en pantalla y dice: «Es la misma regex. Una solo la lee quien la escribió; la otra, cualquiera. Nosotros escribimos de la segunda manera».
re.X la regla de nombres de usuario de la lección 4, [a-z][a-z0-9_]{3,15}, y pon un comentario en cada trozo.pos de re.compile(r"\d+") para encontrar en "no.17 at 03:02" solo los números que aparecen después de at.re.compile(r"badge=(?i)f-02") y luego corrígelo con un flag de rango, (?i:...), para que signifique lo mismo.El rastro del Backtracker
(a+)+$
Así que la has desplegado. La próxima no podrás desplegarla. Mañana a las nueve, abre el correo.
La libreta de la directora
re.I ignora mayúsculas; re.M hace que ^ $ funcionen en cada línea; re.S hace que . incluya el salto de línea. M y S hacen cosas distintas.
re.X ignora espacios y comentarios #. Se conservan los espacios dentro de una clase y \ (barra invertida + espacio). (?#...) es un comentario en cualquier sitio.
re.A limita \w \d \s \b a ASCII. En str, re.U es lo predeterminado; re.L es solo para bytes y no se recomienda.
Combinar: re.I | re.M (re.RegexFlag); sin flags, re.NOFLAG. Los inline como (?im) solo al principio.
Flags de rango: (?i:...) (?-i:...) (?i-s:...). a L u no se pueden apagar. re.DEBUG muestra las tripas del patrón.
re.compile sirve para poner nombre. Acepta pos y endpos, pero ^ solo coincide en el verdadero principio. Atributos: flags groups groupindex pattern.
← Anterior
Lección 10 · El mismo modus operandi: retrorreferencias y condicionales
Siguiente →
Lección 12 · El Backtracker, desenmascarado: repaso y un tokenizador
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.