Errores comunes con expresiones regulares: coincidencias silenciosas, escapes y backtracking

Los peores errores con expresiones regulares no producen un error de sintaxis: coinciden con el texto equivocado y dejan pasar datos incorrectos. Sin anclas, una validación se convierte en una búsqueda de subcadenas; un punto acepta cualquier carácter, y una regex global de JavaScript puede alternar entre verdadero y falso porque guarda estado. Prueba ejemplos y contraejemplos, entiende la capa de escape del lenguaje y mantén los patrones acotados para que una entrada hostil no provoque un backtracking extremo.

Buscar cuando querías validar el texto completo

La mayoría de las API de regex buscan cualquier subcadena que coincida. Un patrón de validación como \d{4} acepta pedido-1234-antiguo si no está anclado o si no usas una función de coincidencia completa. Es un fallo de aceptación silencioso, no un error de la regex.

const flojo = /\d{4}/;
const estricto = /^\d{4}$/;

flojo.test('x1234y');     // true
estricto.test('x1234y');  // false
estricto.test('1234');    // true

En modo multilínea, ^ y $ coinciden en los límites de cada línea, y en algunos motores $ también antes de un salto de línea final. Para validar el string completo, usa una función de coincidencia completa (re.fullmatch en Python) o anclas absolutas.

Prueba siempre texto basura al principio, al final, un salto de línea intermedio y un string vacío. Un patrón probado solo con un ejemplo válido dice poco sobre si sirve para validar.

Cuantificadores codiciosos que cruzan límites

Los cuantificadores * y + son codiciosos: consumen todo lo posible y retroceden hasta que encaja el resto. <.*> aplicado a <b>uno</b><i>dos</i> abarca desde el primer < hasta el último >.

const entrada = '<b>uno</b><i>dos</i>';
entrada.match(/<.*>/)[0];     // toda la entrada
entrada.match(/<.*?>/)[0];    // <b>
entrada.match(/<[^>]*>/)[0];  // <b>, con un límite explícito

Un cuantificador perezoso (.*?) consume menos, pero no es automáticamente correcto: sigue cruzando delimitadores si el resto del patrón lo exige. Una clase negada como [^"]* expresa mejor el límite real en campos entre comillas.

No analices HTML arbitrario con regex. Comillas, comentarios, elementos de texto sin procesar y marcado mal formado necesitan un parser de HTML.

Olvidar la capa de escape del lenguaje

Una regex dentro de un string se analiza dos veces: primero el lenguaje de programación y después el motor de regex. Para que llegue \d a través de un string normal hay que escribir \\d. Los strings raw o los literales de regex eliminan una capa, pero no el escape propio de la regex.

import re

re.search('\\d+', 'id=42')   # string normal: barra duplicada
re.search(r'\d+', 'id=42')    # string raw: lo recomendable

# En ambos casos el motor ve \d+

Si una regex funciona online pero falla en el código, casi siempre se perdió una barra en el string o el probador usaba otros delimitadores y flags. Imprime el patrón final para ver qué llegó al motor.

El texto del usuario debe escaparse con la función del motor (re.escape, preg_quote, Pattern.quote) antes de insertarlo en un patrón. Concatenar un . o un + que escribió alguien cambia la gramática del patrón y puede provocar denegación de servicio.

Malinterpretar clases de caracteres y alternancia

Dentro de [], cada carácter representa una opción de un solo carácter, no una secuencia. [gato] coincide con una g, a, t u o, no con la palabra gato. Un guion puede definir un rango y un acento circunflejo niega la clase si va el primero.

/gr[ai]s/        // gras o gris
/^(gato|perro)$/ // exactamente gato o perro
/^[A-Z-]+$/      // mayúsculas y guion literal
/[^0-9]/         // un carácter que no sea dígito

La alternancia tiene menos precedencia que la secuencia. ^gato|perro$ significa "empieza por gato" o "termina en perro", no "exactamente una de las dos". Agrupa las alternativas: ^(?:gato|perro)$.

Los rangos dependen del orden de los puntos de código, no del alfabeto. [A-z] incluye signos de puntuación entre las mayúsculas y las minúsculas ASCII, y [a-z] no incluye ñ ni vocales con tilde. Para letras en español usa \p{L} con el flag Unicode.

Suponer que el punto, los flags y los límites significan lo mismo en todas partes

El punto suele coincidir con cualquier carácter excepto el salto de línea. El flag dotall (s) le permite cruzar líneas; el flag multilínea (m) cambia las anclas, no el punto. Confundirlos explica muchos patrones que se detienen en el primer salto de línea.

const texto = 'INICIO\nvalor\nFIN';
/INICIO.*FIN/.test(texto);   // false
/INICIO.*FIN/s.test(texto);  // true: dotall
/^valor$/m.test(texto);      // true: anclas multilínea

El flag global g de JavaScript tiene estado con test(): avanza lastIndex. Reutilizar la misma regex puede alternar resultados. Quita g para una comprobación booleana o reinicia lastIndex antes de reutilizarla.

const r = /x/g;
r.test('x');   // true, lastIndex pasa a 1
r.test('x');   // false, la búsqueda empieza en el índice 1
r.lastIndex = 0;

Provocar backtracking catastrófico

Los cuantificadores anidados o solapados pueden obligar a un motor con backtracking a explorar un número exponencial de caminos antes de fallar. Un patrón como ^(a+)+$ parece inofensivo, pero se vuelve extremadamente lento con una larga serie de a seguida de !.

# Arriesgado: los cuantificadores anidados reparten los mismos caracteres de muchas formas
^(a+)+$

# El mismo requisito con una sola repetición
^a+$

Evita grupos repetidos cuyas alternativas coincidan con el mismo prefijo, acota las repeticiones cuando la entrada tenga un máximo conocido y prefiere delimitadores explícitos. Aplica límites de longitud a la entrada y, donde sea posible, tiempos máximos de ejecución. Un probador con ejemplos cortos no demuestra un buen comportamiento en el peor caso: incluye un texto largo que falle en el último carácter.

Depura con una tabla de contraejemplos

Antes de tocar el patrón, apunta textos que deben coincidir y textos que no. Añade casos vacíos, de longitud mínima y máxima, Unicode, multilínea y casi válidos. Cada corrección debe hacer pasar un caso sin romper los anteriores.

patrón: ^[A-Z]{2}-\d{4}$

AB-1234   coincide
ab-1234   rechazar (mayúsculas)
ABC-1234  rechazar (longitud)
AB-123    rechazar (dígitos)
xAB-1234  rechazar (prefijo)
AB-1234x  rechazar (sufijo)

Anota el motor y los flags junto a los casos, porque JavaScript, Python, PCRE, Java y .NET no comparten todas las funciones. Para reglas de negocio complejas, suele ser más claro combinar una regex estructural sencilla con código normal.

Fuentes y lecturas recomendadas

La guía de expresiones regulares de MDN, la documentación del módulo re de Python y la documentación de patrones de PCRE2 describen anclas, flags, clases y escapes. La página de OWASP sobre ReDoS explica el backtracking catastrófico con ejemplos.

Frequently asked questions

¿Por qué mi regex coincide con parte de un texto no válido?

La mayoría de las funciones buscan una subcadena que coincida. Ancla el patrón al texto completo o usa la función de coincidencia completa del motor, y prueba texto basura al principio y al final.

¿Qué es un cuantificador codicioso?

Uno que consume todo el texto posible, como `.*`, y por eso puede cruzar el delimitador donde querías parar. Usa un límite explícito como `[^"]*`; un cuantificador perezoso ayuda, pero no siempre basta.

¿Por qué mi regex funciona online pero no en mi código?

El lenguaje puede consumir una barra invertida antes de que el motor vea el patrón, o el probador usa otro motor o flags. Usa strings raw o literales de regex y revisa el patrón final.

¿Por qué test() de JavaScript devuelve resultados alternos?

Una regex con el flag global o sticky guarda `lastIndex`, y cada `test()` empieza en otra posición. Quita el flag para validar o reinicia `lastIndex` antes de cada prueba.

¿Qué causa el backtracking catastrófico?

Repeticiones anidadas o solapadas que dan al motor demasiadas formas de repartir un texto que casi coincide. Simplifica los grupos ambiguos, limita la entrada y prueba textos largos que fallen al final.

¿Por qué [a-z] no coincide con la ñ ni con letras con tilde?

Los rangos siguen el orden de los puntos de código ASCII, y `ñ` o `é` están fuera de `a-z`. Usa `\p{L}` con el flag Unicode (`u` en JavaScript y PHP), o incluye las letras de forma explícita.