Regex
Aserciones lookbehind en regex explicadas
Lookbehind permite matchear algo precedido por un patrón sin consumirlo. Así funciona en JavaScript, Python, Go, PCRE, y cuándo realmente merece la pena.
Lookbehind es la funcionalidad regex menos comprendida por la mayoría de developers. Es estrecha, ocasionalmente irreemplazable, y se rompe en engines que no te esperas. Esto es un repaso directo al grano: la sintaxis, las trampas y la matriz de engines.
Qué hace lookbehind
Un lookbehind es una aserción de ancho cero: matchea una posición, no caracteres. (?<=foo)bar matchea bar solo cuando está precedido por foo, pero el foo no forma parte del match. El lookbehind negativo (?<!foo)bar matchea bar solo cuando NO está precedido por foo.
La clave es que el contexto precedente no se consume. Si hicieras un replace del match, solo reemplazarías bar, no foobar.
// Match "100" solo cuando está precedido por "$"
const re = /(?<=\$)\d+/g;
"price is $100, quantity 200".match(re);
// => ["100"]
// Negativo: dígitos NO precedidos por "$"
const re2 = /(?<!\$)\b\d+/g;
"price is $100, quantity 200".match(re2);
// => ["200"]
Sin lookbehind tendrías que capturar \$(\d+) y coger el grupo 1 del match object, lo cual está bien para un match suelto pero se vuelve incómodo para replace, split o pipelines con matchAll.
Matriz de soporte por engine
Aquí es donde la gente se quema. Lookbehind no es universal.
| Engine | Lookbehind | Longitud variable | Notas |
|---|---|---|---|
| JavaScript (V8, SpiderMonkey) | Sí | Sí | ES2018; Chrome 62+, Node 10+, Safari 16.4+ |
Python re | Sí | No | Solo longitud fija; el módulo regex levanta la restricción |
Python regex (PyPI) | Sí | Sí | Reemplazo drop-in |
| PCRE / PCRE2 | Sí | No (PCRE), sí (PCRE2 parcial) | Usado por PHP, nginx, muchas tools C |
.NET Regex | Sí | Sí | Siempre soportado |
Java java.util.regex | Sí | Acotado | Requiere cuantificadores acotados dentro |
Go regexp (RE2) | NO | — | Sin lookaround en absoluto |
Rust regex crate | NO | — | Misma razón que Go: garantía de tiempo lineal |
| ripgrep por defecto | NO | — | Usa Rust regex; activa PCRE2 con -P |
Go y Rust descartan lookbehind a propósito. Sus engines de regex garantizan tiempo lineal sobre el input; lookaround impide esa garantía. Si usas rg y necesitas lookbehind, activa rg -P para cambiar a PCRE2.
Lookbehind de longitud variable
La queja histórica sobre lookbehind era la restricción de longitud fija. (?<=abc) siempre ha funcionado. (?<=a{1,3}) fallaba en la mayoría de engines. Eso cambió:
- JavaScript permite longitud variable desde el primer día de la spec ES2018.
- El
reestándar de Python sigue exigiendo longitud fija. Cambia al móduloregexde PyPI si necesitas alternación de longitudes distintas. - .NET siempre lo ha permitido.
- Java requiere un límite superior acotado, p. ej.
(?<=a{1,100}).
# Python stdlib: esto FALLA
import re
re.search(r"(?<=ab|abcd)X", "abcdX")
# re.error: look-behind requires fixed-width pattern
# Módulo regex de Python: funciona
import regex
regex.search(r"(?<=ab|abcd)X", "abcdX")
# <regex.Match ...>
Usos reales habituales
Tres patrones cubren el 90% del uso legítimo de lookbehind:
- Extraer un token después de un marcador sin mantener el marcador. Símbolos de moneda, prefijos tipo
id-, tags de nivel de log. - Splittear por un delimitador que quieres conservar en el lado derecho.
str.split(/(?=\n## )/)splittea un Markdown antes de cada##heading sin comérselo. - Evitar falsos positivos alrededor de word boundaries que
\bno puede expresar. Ejemplo: matchealogpero no cuando está precedido porsyslog, es decir(?<!sys)log.
Para la referencia diaria de metacaracteres, cuantificadores y anclas, el cheatsheet de regex cubre el resto.
Cuándo lookbehind es la herramienta equivocada
Que lookbehind exista no significa que tengas que tirar de él.
- Si el engine no lo soporta (Go, Rust
regex, ripgrep por defecto), reescribe con un grupo de captura.\$(\d+)con extracción del grupo 1 es equivalente a(?<=\$)\d+para la mayoría de casos. - Si el contexto precedente es una palabra o línea entera, las anclas y word boundaries son más baratas:
^WARN:es mejor que(?<=^)WARN:. - Si escaneas un log enorme, el lookaround en un engine backtracking (PCRE, Python) puede explotar en runtimes patológicos sobre input adversarial. Los engines estilo RE2 (Go, Rust) lo rechazan por diseño.
- Si acabas anidando tres lookbehinds, lo que quieres en realidad es un parser.
Rendimiento en la práctica
Lookbehind no es gratis. En un engine backtracking (PCRE, re de Python, Java) un lookbehind de ancho N cuesta aproximadamente N veces el trabajo base en cada posición candidata porque el engine tiene que retroceder N caracteres e intentar el sub-patrón. El lookbehind de longitud variable multiplica esto — el engine prueba cada longitud del rango permitido.
Para hot paths — parsers de log, scans de CSV grandes, analizadores de código — esto importa. Tres guías aproximadas:
- Lookbehind de longitud fija 1-3: efectivamente gratis.
- Fija 4-20: aún barato en CPUs modernas.
- Longitud variable con rangos amplios: mide, especialmente con input adversarial.
Si escribes regex que corre una vez por request HTTP, ignora el rendimiento y optimiza legibilidad. Si escribes regex que recorre gigabytes, benchmarkéalo.
Depurando lookbehind en la práctica
Dos trucos que ahorran horas:
Usa matchAll en JavaScript, no match, cuando necesites offsets. match con flag global descarta los grupos de captura.
const re = /(?<=\$)(\d+(?:\.\d+)?)/g;
for (const m of "$1.50 and $42".matchAll(re)) {
console.log(m[1], m.index);
}
// 1.50 1
// 42 11
Testea tu patrón contra al menos tres strings: el caso positivo, el caso negativo con prefijo equivocado, y el caso borde en inicio de string. El lookbehind en índice 0 falla silenciosamente si el contexto precedente es obligatorio; suele ser lo que quieres, pero confírmalo.
Un bug sorprendentemente habitual: ingenieros testean a mano una regex con lookbehind en las DevTools del navegador y funciona, luego la pegan en código server-side Go o Rust y el patrón falla silenciosamente al compilar o no matchea nada. Corre siempre el patrón en el engine exacto que usará tu código.
Conclusiones
Lookbehind merece el coste mental. Usa (?<=...) cuando necesites contexto de prefijo sin consumirlo, (?<!...) cuando necesites ausencia de prefijo. Chequea tu engine primero: Go y Rust dicen no, el Python stdlib dice solo ancho fijo, el resto va bien. Para material de referencia largo bookmarkea el cheatsheet de regex; para patrones de slug y URL donde encoding se cruza con regex, mira la guía de URL encoding.