Paso a paso · 6 min de lectura
Expresiones regulares: una guía práctica de inicio
Una expresión regular es un patrón que describe un conjunto de cadenas. Puede comprobar si un texto coincide, encontrar todas las apariciones, capturar partes de una coincidencia o buscar y reemplazar con referencias a esas capturas. La mayoría de los lenguajes de programación y muchas herramientas de línea de comandos implementan la misma sintaxis básica con pequeñas diferencias.
La dificultad no está en escribir un patrón una vez, sino en entender por qué un patrón coincide más o menos de lo que pretendías. Esta guía cubre los bloques de construcción, el modelo de coincidencia que explica resultados sorprendentes y los límites de seguridad que conviene conocer antes de ejecutar un patrón sobre entrada no confiable.
Una expresión regular es una descripción, no una cadena de búsqueda
Una búsqueda literal encuentra los caracteres exactos que escribiste. Una expresión regular interpreta algunos caracteres como instrucciones, así que el patrón abc coincide con los caracteres abc, mientras que a.c coincide con cualquier cadena de tres caracteres que empiece por a y termine por c. Esa interpretación es lo que hace potente a regex y lo que convierte un punto sin escapar en un error habitual.
Las expresiones regulares suelen aplicarse a una línea o a una cadena completa según los flags, y ese contexto cambia el resultado. El mismo patrón puede coincidir con una subcadena en una sola línea y con algo completamente distinto una vez incluidos los saltos de línea. Confirma siempre si el motor trata la entrada como una sola cadena o como líneas separadas antes de depurar el patrón en sí.
Los bloques de construcción que conviene aprender primero
Las clases de caracteres coinciden con un carácter de un conjunto: [abc] coincide con a, b o c; [a-z] coincide con cualquier letra ASCII minúscula; \d es un dígito; \w es un carácter de palabra; \s es espacio en blanco. Las clases negadas invierten el significado, así que [^0-9] coincide con cualquier cosa que no sea un dígito. Los anclajes no consumen caracteres: ^ coincide con el inicio de la cadena o de la línea y $ con el final.
Los cuantificadores indican cuántas veces se repite el elemento anterior: * significa cero o más, + significa uno o más, ? significa cero o uno, y {2,5} significa entre dos y cinco. Por defecto son codiciosos y toman lo máximo posible, y solo devuelven caracteres si el resto del patrón los necesita; añadir ? los hace perezosos y toman lo mínimo posible. Los grupos entre paréntesis capturan lo que coincidió, y una barra vertical crea alternativas como cat|dog.
- \d+ coincide con uno o más dígitos; ^\d{5}$ coincide exactamente con cinco dígitos por sí solo.
- \bword\b coincide con la palabra completa, no con la palabra dentro de otra palabra.
- ([A-Za-z]+)\s+([A-Za-z]+) captura dos palabras separadas por espacio en blanco.
- a.*?b coincide desde la primera a hasta la b más cercana, mientras que a.*b llega hasta la última b.
- Los flags cambian el comportamiento: i ignora mayúsculas y minúsculas, g encuentra todas las coincidencias, m trata ^ y $ como anclajes de línea, s permite que el punto coincida con saltos de línea y u activa la semántica Unicode completa.
Cómo avanza realmente la coincidencia
Los motores prueban el patrón en posiciones sucesivas de la entrada (salvo que esté anclado), y en cada posición exploran alternativas. Cuando un cuantificador codicioso ha consumido caracteres y el resto del patrón falla, el motor retrocede y prueba una repetición más corta. Ese retroceso es la razón por la que un patrón puede coincidir correctamente y aun así ser lento: el número de caminos que explora puede crecer de forma exponencial con la entrada.
La coincidencia global es un bucle alrededor de ese proceso. Un patrón con el flag g devuelve coincidencias sucesivas, y las coincidencias de longitud cero avanzan la posición de forma especial para evitar un bucle infinito. Los grupos de captura se numeran por el orden en que aparece su paréntesis de apertura, y los grupos con nombre hacen que los patrones complejos sean mucho más fáciles de leer y mantener.
Un flujo de trabajo de depuración que funciona
Empieza por la entrada más pequeña que debería coincidir y por la entrada más cercana que no debería. Prueba ambas y después añade complejidad un elemento a la vez en lugar de escribir el patrón completo y quedarte mirando el resultado. Cuando una coincidencia es incorrecta, hazte tres preguntas: dónde empezó, dónde terminó y qué alternativa o repetición consumió los caracteres intermedios.
Escapa los caracteres que el motor trata de forma especial. Esta es la causa más común de confusión al probar URL, rutas de archivo, direcciones de correo y fragmentos de código. Recuerda que los literales de cadena de JSON y de los lenguajes de programación también procesan barras invertidas, así que un patrón escrito para un motor regex puede necesitar la barra invertida duplicada en el código fuente. Usa un probador que muestre tanto el patrón que escribiste como el patrón que recibió el motor.
Seguridad: backtracking catastrófico y entrada no confiable
Algunos patrones son correctos pero catastróficamente lentos con entradas concretas. Los cuantificadores anidados como (a+)+$ son el ejemplo clásico: para una secuencia larga de caracteres seguida de uno que no puede coincidir con el anclaje final, el motor prueba un número exponencial de formas de dividir la secuencia. Las defensas prácticas son simplificar la repetición anidada, anclar los patrones cuando sea posible, preferir clases de caracteres explícitas y aplicar un tiempo de espera o un límite de longitud de entrada al hacer coincidir texto no confiable.
Nunca uses una expresión regular como única defensa para análisis sensibles a la seguridad. Las direcciones de correo, el HTML, las URL y los lenguajes de programación superan lo que un patrón compacto puede validar correctamente. Usa regex para una comprobación rápida de cordura y después analiza con una biblioteca dedicada o valida estructuralmente. Y cuando un patrón se ejecuta en un servidor, limita el tamaño de la entrada y el número de coincidencias para que una sola petición no pueda consumir el proceso.