R

ReflexKR

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

Tiempo 8:20–9:10

La regex ilegible: flags y compilación

CASE-011

Grep & Co. Agencia de Detectives de Datos

Expediente

La regex ilegible

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

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

11-1. Mayúsculas y líneas: re.I, re.M (8 min)

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

No toques el patrón; prueba solo los botones de flags. Con re.M, ^ y $ coinciden en cada línea; con re.I, también entra la línea de f-02 en minúsculas.

11-2. re.S abre la ventana (6 min)

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)    # None

Dot 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

Pulsa el botón re.S para abrir la ventana. Cuando . cruza los saltos de línea, las tres líneas se capturan de una vez.

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.

11-3. Escribir con holgura: re.X y (?#...) (9 min)

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-02

Es 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

Es un patrón escrito con espacios. Sin re.X, los espacios también son caracteres y no encuentra nada. Enciende re.X. El espacio dentro de [ ] se conserva incluso con re.X.

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

11-4. El alcance de los caracteres: re.A, re.U, re.L (5 min)

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 pattern

Laboratorio de pruebas

re.search(r"", text)

Flags

Valor devuelto

Enciende y apaga re.A y mira qué se le escapa a \w+. Cambia el patrón por \d para comprobar también el dígito arábigo ٣.

11-5. Combinar, encender y apagar flags (6 min)

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 6

Para 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 4

Por ú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_END

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

11-6. re.compile y el objeto Pattern (6 min)

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))))   # 1

pos 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)   # True
  • Las funciones del módulo, como re.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).
  • Así que la verdadera ventaja de re.compile no es la velocidad, sino el nombre. Un patrón llamado DOOR se entiende al leer el código.
  • Para las anotaciones de tipo, usa 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)))   # 2

Caso resuelto (10 min)

Vuelves 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-02

La 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».

Ejercicios

  1. Despliega con 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.
  2. Usa pos de re.compile(r"\d+") para encontrar en "no.17 at 03:02" solo los números que aparecen después de at.
  3. Comprueba el error de 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.

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