Regex en la práctica: anclas, cuantificadores y grupos de captura
Resumen (TL;DR)
El 2 de julio de 2019, una sola regex dentro del WAF de Cloudflare —un patrón con forma .*(?:.*=.*)— tiró cerca del 50% del tráfico global durante 27 minutos. El backtracking de PCRE clavó un core de CPU al 100% y cada petición se atascó detrás de eso. La lección más honesta que enseña regex es que oculta su propio costo. La mayoría de los días es pequeña, rápida y seductora; una entrada mala puede detener un sistema. Por eso este post tiene un tono ligeramente escéptico. La mayor parte del trabajo regex cotidiano descansa en un puñado de piezas: anclas que atan un patrón al inicio, al final o a un límite de palabra; clases de caracteres que describen “cualquiera de estos caracteres”; cuantificadores que dicen “cuántos”; y grupos que permiten capturar, referenciar o alternar. Hazlos bien y los problemas típicos —validar un email aproximado, extraer campos de una línea de log, normalizar un número de teléfono— se vuelven cortos y legibles. Hazlos mal y terminas con patrones que coinciden demasiado, demasiado poco, o detienen el motor como en el caso de Cloudflare. No parsees HTML, JSON ni XML con regex: los lenguajes regulares no pueden describir anidación balanceada. Los motores también difieren: la regex de ECMAScript, PCRE (Perl/PHP), re de Python, Oniguruma (Ruby y la crate onig de Rust) y RE2 de Go tienen cada uno diferencias sutiles. El incidente de Cloudflare solo ocurrió porque el motor era PCRE; bajo RE2 se habría completado en tiempo lineal.
Antecedentes y conceptos
Una expresión regular es una gramática compacta para emparejar cadenas. Las piezas que usas con más frecuencia vale la pena nombrarlas explícitamente.
Las anclas no consumen caracteres; afirman una posición. ^ es el inicio de la entrada (o inicio de línea en modo multilínea), $ es el final, y \b es un límite de palabra (la transición entre \w y no-\w). Sin anclas, abc coincide en cualquier parte dentro de una cadena más larga. Con ellas, ^abc$ coincide solo con la cadena entera literal abc, y \babc\b coincide con abc como palabra autónoma.
Las clases de caracteres describen un único carácter de un conjunto. [abc] es a, b o c; [a-z] es cualquier letra minúscula; [^0-9] es “cualquier cosa salvo un dígito”. Las clases abreviadas cubren los casos comunes: \d (dígitos), \w (caracteres de palabra: letras, dígitos, guión bajo), \s (espacio en blanco). Las negaciones \D, \W, \S son las inversas. . coincide con cualquier carácter excepto una nueva línea por defecto; el flag s (dotall) lo cambia.
Los cuantificadores se adhieren al token previo y dicen cuántas veces puede repetirse. * es cero o más, + es uno o más, ? es cero o uno, y {n,m} es “entre n y m” (cualquiera de las cotas puede omitirse). Por defecto los cuantificadores son voraces: coinciden tanto como sea posible y solo ceden si el resto del patrón falla. Añade ? para la forma perezosa —*?, +?, ??— que coincide tan poco como sea posible y solo se extiende cuando se le fuerza. Algunos motores añaden cuantificadores posesivos (*+, ++) que coinciden voraces pero se niegan a ceder en caso de fallo, lo que puede prevenir el backtracking catastrófico ante entradas patológicas; exactamente la herramienta que faltaba en el incidente de Cloudflare.
Los grupos envuelven subpatrones. (patrón) es un grupo capturador —numerado desde 1 en orden del paréntesis de apertura— cuya coincidencia puede referenciarse con \1 dentro del patrón o con accesores indexados en el lenguaje anfitrión. (?<nombre>patrón) añade una captura con nombre. (?:patrón) es un grupo no capturador, usado puramente para alternación ((?:cat|dog)) o agrupamiento de cuantificador sin registrar la coincidencia. | dentro de un grupo es alternación: coincide con una de las alternativas.
Finalmente, los flags cambian el comportamiento del motor: i para no distinguir mayúsculas, m para multilínea (^ y $ coinciden con límites de línea), s para dotall, u para Unicode. En JavaScript, el flag u también habilita escapes de propiedades Unicode como \p{L} para “cualquier letra”.
Comparación y datos
| Cuantificador | Voraz (por defecto) | Perezoso (sufijo ?) | Posesivo (donde está soportado) |
|---|---|---|---|
* / *? / *+ | Coincide con tantos como sea posible, retrocede en fallo | Coincide con tan pocos como sea posible, extiende en fallo | Coincide con tantos como sea posible, sin retroceso |
+ / +? / ++ | Uno o más, voraz | Uno o más, perezoso | Uno o más, sin retroceso |
? / ?? / ?+ | Cero o uno, prefiere uno | Cero o uno, prefiere cero | Cero o uno, sin retroceso |
{n,m} / {n,m}? / {n,m}+ | Rango, voraz | Rango, perezoso | Rango, sin retroceso |
Voraz es el valor por defecto correcto suficientes veces como para serlo. La pereza importa cuando “el resto del patrón” es en sí mismo permisivo; por ejemplo, extraer <b>...</b> de un extracto HTML con <b>(.*?)</b> en lugar de un .* voraz que podría tragarse múltiples etiquetas (y aun así, no parsees HTML real con regex). Los cuantificadores posesivos y los grupos atómicos (?>...) ayudan cuando un patrón de otra manera exploraría exponencialmente muchos caminos de retroceso ante casi-coincidencias. PCRE, Java y Oniguruma los soportan; ECMAScript y re de Python históricamente no, aunque Python 3.11 añadió grupos atómicos al módulo re.
Escenarios reales
Escenario 1 — Una coincidencia pragmática de email. La gramática completa de email de RFC 5322 admite comentarios, partes locales entre comillas y literales IP anidados; una regex que lo cubre todo es notoriamente monstruosa (el intento más famoso tiene 6 425 caracteres) y aun así no es un parser real. El patrón que realmente he desplegado en producción es casi siempre ^[^\s@]+@[^\s@]+\.[^\s@]+$ —“no vacío, sin espacios, sin @ perdidos, con al menos un punto en el dominio”— que rechaza los errores tipográficos obvios sin pretender validar del todo. La única forma de verificar de verdad una dirección es enviarle un mensaje. Usa regex para la forma, email para la existencia.
Escenario 2 — Números de teléfono con formatos internacionales. +82 2-1234-5678, (02) 1234-5678 y 82-2-1234-5678 describen todos el mismo número de Seúl, Corea. Una regex como ^\+?\d{1,3}[-\s().]*\d{1,4}[-\s().]*\d{3,4}[-\s().]*\d{3,4}$ acepta la puntuación común y luego un paso de normalización quita la puntuación a una forma canónica solo-dígitos. Para cualquier cosa seria —enrutamiento, almacenamiento, marcado— usa libphonenumber de Google en vez de hacerla tú mismo. El desenlace más rápido de reunión que he visto en este tema fue “no vamos a hacer esto con regex”, lo que ahorró al equipo una semana de bugs de casos límite. La regex es para la superficie de “¿esto se parece vagamente a un número de teléfono?”.
Escenario 3 — Extraer campos de una línea de log. Una línea como 2026-04-13T02:11:05Z 192.0.2.42 "GET /search?q=foo HTTP/1.1" 200 1534 puede dividirse con un único patrón: ^(?<ts>\S+)\s+(?<ip>\S+)\s+"(?<method>\w+)\s+(?<path>\S+)\s+\S+"\s+(?<status>\d+)\s+(?<bytes>\d+)$. Los grupos con nombre se pagan aquí: el objeto de coincidencia resultante es como un diccionario y cada campo es direccionable por nombre. Cuando el formato del log cambia, el patrón también es la documentación de lo que estás parseando.
Errores comunes
“Una regex puede validar por completo una dirección de email.” Solo puede validar la forma. RFC 5322 es demasiado complejo para codificar sensatamente en una regex, y aunque lo hicieras, “la forma es válida” no significa “el buzón existe”. El patrón estándar de la industria es una regex simple más un email de verificación.
“Voraz siempre es más lento que perezoso.” No inherentemente. Las coincidencias voraces pueden ser más rápidas cuando el subpatrón del cuantificador es muy restrictivo, porque el motor termina en una única pasada larga hacia adelante. La pereza gana cuando “el resto del patrón” ancla la coincidencia, como en <b>(.*?)</b>. Haz benchmark con entradas realistas en vez de recurrir a ? de forma refleja.
“Todos los motores regex son iguales.” No lo son. La regex de ECMAScript carece de cuantificadores posesivos y grupos atómicos (el flag v en motores modernos cierra algunos huecos pero no esos); re de Python tiene su propio conjunto de propiedades Unicode; PCRE soporta back-references y patrones recursivos; Oniguruma —el motor usado por Ruby y la crate onig de Rust— es otro dialecto más. RE2 de Go abandona las back-references y los lookarounds a cambio de tiempo de ejecución garantizadamente lineal en la entrada; el mismo motor que Cloudflare evaluó migrar tras su incidente. Un patrón que copias de un tutorial de Perl puede no funcionar en JavaScript, y viceversa.
“Regex puede parsear HTML (o JSON, o XML).” No, porque los lenguajes regulares no pueden describir anidación balanceada. La regex puede extraer un subpatrón específico y bien formado de texto estructurado —un valor de atributo único, por ejemplo— pero no puede parsear correctamente todo el árbol. Para formatos anidados, usa un parser dedicado (DOMParser, JSON.parse, una librería XML, un lector CSV). La saga de Stack Overflow de “regex vs HTML” (la respuesta de 2009) es un cuento con moraleja, no un debate.
Lista de verificación
- ¿Cuál es la entrada y cuáles los contraejemplos? Escribe ambos antes de escribir el patrón.
- ¿El dato está anidado o es recursivo? Si sí, usa un parser. La regex es la herramienta equivocada.
- ¿Qué motor estás apuntando? JavaScript, Python, Go, PCRE difieren cada uno en lookaround, back-references, Unicode.
- ¿Necesitas la coincidencia en sí o solo un sí/no? Prefiere
(?:...)no capturador para grupos que solo existen para alternación o cuantificación. - ¿El patrón lo suministra el usuario o se aplica a entrada no confiable? Protégete contra backtracking catastrófico con un límite de tiempo, grupos atómicos o un motor de tiempo lineal como RE2. De otro modo te vuelves Cloudflare.
- ¿Realizas normalización de texto después? No codifiques todo en un único patrón enorme; empareja una comprobación de forma simple con un pequeño paso de postprocesado.
- ¿Está la regex documentada? El modo multilínea con
x(extendido) o un comentario encima del patrón es un seguro barato para la siguiente persona que lo lea.
Herramienta relacionada
El tester de regex de Patrache Studio corre patrones contra entrada de muestra en el navegador y muestra las capturas de grupo en línea, lo cual es más rápido que cambiar entre pestañas. Cuando las cadenas que emparejas están a su vez estructuradas —líneas de log con payloads JSON, respuestas de API— combina el trabajo regex con Formateo, validación y esquema JSON en la práctica para que la pieza extraída se valide con un parser adecuado en vez de con una segunda regex. Un objetivo común de regex es un UUID embebido en una URL; UUID v1 vs v4 vs v7: elegir una clave primaria de BD cubre por qué los mismos 36 caracteres pueden significar cosas diferentes según los bits de versión.
Referencias
- MDN, guía de “Regular Expressions” — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions
- IETF RFC 5322, “Internet Message Format” (gramática de email) — https://datatracker.ietf.org/doc/html/rfc5322
- regex101, tester interactivo con selección de sabor — https://regex101.com/
- Google, RE2 — motor regex de tiempo lineal — https://github.com/google/re2/wiki/Syntax