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 5:50–6:40
CASE-008
Grep & Co. Agencia de Detectives de Datos
Expediente
Cliente
Dejado en la puerta de la agencia (remitente: el Backtracker)
Pruebas
Un sobre negro. Dentro, una tarjeta con una regex de una línea y una pregunta.
Para cerrar el caso
Explicar por qué el patrón de la tarjeta es lento y responder con un patrón que encuentre las mismas cadenas pero termine al instante.
Por la mañana hay un sobre negro encajado en la rendija de la puerta. Dentro, una sola tarjeta. La directora la levanta a contraluz y luego la deja sobre el escritorio. «Por fin se presenta. Con una firma en lugar de una cara».
^(a|aa)+$
이 패턴은 정답을 맞힌다. 그런데 왜 이렇게 느린가?
답은 패턴으로 보내라.
(a+)+$La tarjeta está escrita en coreano: «Este patrón acierta. Entonces, ¿por qué es tan lento? Responde con un patrón».
«Para resolverlo hay que saber cómo piensa un motor de regex», dice la directora. «Hoy vamos a asomarnos a su cabeza».
El motor de regex va encajando el patrón en la cadena pieza a pieza, de izquierda a derecha. Si algo no encaja, no se rinde enseguida: deshace la última decisión que tomó y prueba otro camino. A esto se le llama retroceso (backtracking).
En la lección 3 vimos que los cuantificadores, por defecto, «comen todo lo que pueden» (son codiciosos). Veamos qué pasa al extraer etiquetas de un HTML.
html = "<b>bold</b> and <i>it</i>"
re.findall(r"<.+>", html) # ['<b>bold</b> and <i>it</i>']En lugar de cuatro etiquetas, se capturó toda la línea de una vez. Si seguimos lo que hizo el motor, queda así:
< encaja con el primer <..+ es codicioso y se come hasta el final de la línea.> del patrón, pero no queda ningún carácter. Fallo..+ va devolviendo los caracteres de uno en uno y vuelve a intentarlo. En cuanto recupera el último >, lo consigue.El resultado va «desde el < más exterior hasta el último >». El que se equivoca no es el motor, sino el patrón.
Laboratorio de pruebas
re.search(r"", text)
Valor devuelto
Si añades ? detrás de un cuantificador, se convierte en un cuantificador perezoso (lazy), que come lo menos posible. Primero toma lo mínimo y solo come un carácter más cuando lo que sigue no encaja.
| Codicioso | Perezoso | Lo que prueba primero |
|---|---|---|
* | *? | 0 veces |
+ | +? | 1 vez |
? | ?? | 0 veces |
{m,n} | {m,n}? | m veces |
re.findall(r"<.+?>", html) # ['<b>', '</b>', '<i>', '</i>']
re.findall(r"a+?", "aaa") # ['a', 'a', 'a']
re.findall(r"a??", "a") # ['', 'a', '']
re.search(r"\d{2,4}?", "12345").group() # '12'Un cuantificador perezoso cambia «dónde se detiene». Por eso viene bien para capturar en corto cosas con un final bien marcado, como las etiquetas.
Pongamos lado a lado las formas de extraer etiquetas.
| Patrón | Resultado | En una frase |
|---|---|---|
<.+> | Una sola pieza: la línea entera | Es codicioso y come demasiado |
<.+?> | Cuatro etiquetas | Se detiene por pereza |
<[^>]+> | Cuatro etiquetas | Ni siquiera puede comerse un > |
<[^>]+> usa la clase negada de la lección 2 y solo come «caracteres que no son >». Casi nunca tiene que retroceder, así que es la más rápida y la más clara. La directora lo resume así: «Mejor exacto que perezoso».
El consejo de Dot
Los cuantificadores perezosos no son magia. Solo cambian dónde termina la coincidencia; cuando no hay coincidencia, el motor retrocede las mismas veces. Si un patrón es lento, hacerlo perezoso no lo vuelve rápido.
Ahora miremos el patrón de la tarjeta, ^(a|aa)+$. Busca «una cadena formada entera por una o más a seguidas». Significa lo mismo que ^a+$.
bool(re.fullmatch(r"^(a|aa)+$", "aaaa")) # True
bool(re.fullmatch(r"^a+$", "aaaa")) # TrueEl problema aparece cuando se encuentra con una cadena que no encaja. Si al final hay una b, como en aaaa…ab, el motor prueba todas las maneras de repartir las a entre a y aa antes de admitir el fallo. Y el número de repartos crece de forma explosiva con cada a que se añade.
Estos son los tiempos medidos en la computadora de la agencia (Python 3.14):
| Número de a | ^(a|aa)+$ | ^a+$ |
|---|---|---|
| 16 | 0.25ms | menos de 0.04ms |
| 20 | 1.1ms | menos de 0.01ms |
| 24 | 7.2ms | menos de 0.01ms |
| 28 | 51ms | menos de 0.01ms |
Cada 4 a más, es unas 7 veces más lento. A ese ritmo, con 40 a pasa con creces de los 10 segundos. Una repetición con opciones que se solapan es la forma típica que hace explotar el retroceso. Las repeticiones dentro de repeticiones, como (a+)+, son de la misma familia.
Laboratorio de pruebas
re.search(r"", text)
Valor devuelto
Desde Python 3.11, si añades + detrás de un cuantificador obtienes un cuantificador posesivo. Lo que se ha comido no lo devuelve aunque lo que sigue falle. Elimina de raíz los caminos de vuelta.
re.fullmatch(r"a*a", "aaa") # <re.Match object; span=(0, 3), match='aaa'>
re.fullmatch(r"a*+a", "aaa") # Nonea*+a no coincide con ninguna cadena. a*+ se queda con todas las a y no las devuelve, así que a la a final no le queda nada con que encajar. Pero justamente esa propiedad permite arreglar el patrón de la tarjeta.
re.search(r"^(?:a|aa)++$", "a" * 28 + "b") # None (termina al instante)++ hace posesiva la repetición del grupo: si falla, no prueba otros repartos y se rinde enseguida.
El grupo atómico (atomic group) (?>...) también existe desde la 3.11. Cuando lo de dentro del grupo coincide una vez, ese resultado queda fijado en bloque y el motor ya no vuelve a entrar. De hecho, un cuantificador posesivo es una abreviatura de un grupo atómico: a++ equivale a (?>a+).
re.fullmatch(r"(?>a+)b", "aab") # <re.Match object; span=(0, 3), match='aab'>
re.fullmatch(r"(?>a+)a", "aaa") # NonePreparas la respuesta con la directora. Hay tres maneras de arreglar un patrón lento.
^a+$ en vez de ^(a|aa)+$.^(?:a|aa)++$, o un grupo atómico, ^(?>(?:a|aa)+)$.regex tiene un argumento timeout.)import time
text = "a" * 40 + "b"
start = time.perf_counter()
print(re.search(r"^a+$", text)) # None
print(time.perf_counter() - start < 0.01) # TrueEscribes ^a+$ en el reverso de la tarjeta y la metes en el sobre. La directora asiente. «Corta, legible y rápida. Buena respuesta».
Informe de caso cerrado — Se comprobó que el patrón del desafío, ^(a|aa)+$, tarda un tiempo que crece exponencialmente cuando falla, por culpa de sus opciones solapadas. Se respondió con ^a+$, que significa lo mismo. Se decide mantener el límite de tiempo del laboratorio.
re.findall(r'".+"', 'say "hi" and "yo"') y corrígelo de dos maneras (cuantificador perezoso y clase negada) para que capture cada cita entre comillas por separado.(\d+)+$ tiene una forma peligrosa y escribe un patrón seguro que signifique lo mismo.findall con a{2,4}? y con a{2,4}+ sobre "aaaaa", ¿qué sale en cada caso? Compruébalo en el laboratorio.El rastro del Backtracker
(a+)+$
Respuesta recibida. El próximo problema es la caja fuerte de su agencia. La contraseña sigue tres reglas, en cualquier orden. Si son capaces de comprobarlas todas desde el mismo sitio.
La libreta de la directora
El motor prueba desde la izquierda y, si se atasca, deshace la última decisión (retroceso, backtracking).
* + ? {m,n} son codiciosos. Con ? detrás se vuelven perezosos: *? +? ?? {m,n}?
Si el final está bien marcado, como en una etiqueta, una clase negada como <[^>]+> es lo más claro y rápido.
Las repeticiones solapadas como (a|aa)+ o (a+)+ disparan el tiempo cuando fallan. Hacerlas perezosas no sirve.
Los cuantificadores posesivos *+ ++ ?+ {m,n}+ y el grupo atómico (?>...) no devuelven nada (3.11+). a++ es (?>a+).
Con patrones ajenos, limita la longitud de la entrada y ejecútalos donde haya un límite de tiempo.
← Anterior
Lección 7 · La víspera de la noticia: sustitución y división
Siguiente →
Lección 9 · Las reglas de la caja fuerte: lookaround
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.