Fundamentos de expresiones regulares
Las expresiones regulares (regex o regexp) son patrones que describen conjuntos de cadenas de texto. Cada lenguaje de programación moderno las soporta, y aunque la sintaxis base es similar, existen diferencias sutiles entre implementaciones (PCRE, JavaScript, Python, .NET). Esta guía se centra en la sintaxis compatible con JavaScript y PCRE, que cubre la mayoría de casos de uso.
Un regex en su forma más simple es una cadena literal: el patrón "hola" coincide exactamente con la secuencia h-o-l-a dentro de un texto. La potencia real llega cuando introduces metacaracteres: el punto (.) coincide con cualquier carácter excepto salto de línea, la barra invertida (\) escapa metacaracteres, y los corchetes ([]) definen clases de caracteres personalizadas.
El error más frecuente entre principiantes es olvidar que regex trabaja carácter a carácter, de izquierda a derecha, y por defecto es codicioso (greedy): intenta consumir la mayor cantidad de texto posible. Entender este comportamiento de backtracking es fundamental para escribir patrones eficientes y evitar el temido "catastrophic backtracking" que puede colgar tu aplicación.
Metacaracteres y clases de caracteres
Los metacaracteres son el vocabulario básico de regex. El punto (.) coincide con cualquier carácter excepto \n. Los atajos predefinidos simplifican patrones comunes: \d equivale a [0-9] (dígitos), \w equivale a [a-zA-Z0-9_] (caracteres de palabra), \s equivale a [ \t\r\n\f] (espacios en blanco). Sus versiones negadas (\D, \W, \S) coinciden con lo opuesto.
Las clases de caracteres entre corchetes permiten definir conjuntos personalizados. [aeiou] coincide con cualquier vocal minúscula. [a-zA-Z] coincide con cualquier letra. El circunflejo dentro de corchetes niega la clase: [^0-9] coincide con cualquier carácter que NO sea un dígito. Los rangos se definen con guión: [a-f0-9] coincide con caracteres hexadecimales en minúscula.
Un detalle importante: dentro de corchetes, la mayoría de metacaracteres pierden su significado especial. El punto dentro de [.] es un punto literal, no "cualquier carácter". Las excepciones son: ] (cierra la clase), \ (escape), ^ (negación al inicio) y - (rango entre caracteres). Para incluir un guión literal, colócalo al inicio o final: [-a-z] o [a-z-].
Las propiedades Unicode (\p{...}) permiten coincidir con categorías de caracteres de forma más precisa en motores que las soportan: \p{L} coincide con cualquier letra en cualquier idioma, \p{N} con cualquier dígito (incluidos dígitos árabes o thai), \p{Emoji} con emojis. Esto es especialmente útil para validar entrada en aplicaciones multilingües.
Metacaracteres principales:
. → Cualquier carácter (excepto \n)
\d → Dígito [0-9]
\D → No dígito [^0-9]
\w → Carácter de palabra [a-zA-Z0-9_]
\W → No carácter de palabra
\s → Espacio en blanco [ \t\r\n\f]
\S → No espacio en blanco
\b → Límite de palabra
\B → No límite de palabra
Clases de caracteres:
[abc] → a, b, o c
[^abc] → Cualquier carácter excepto a, b, c
[a-z] → Cualquier minúscula
[A-Z] → Cualquier mayúscula
[0-9] → Cualquier dígito
[a-f0-9] → Carácter hexadecimalCuantificadores y repetición
Los cuantificadores controlan cuántas veces debe aparecer un elemento. Los tres básicos son: * (cero o más), + (uno o más), y ? (cero o uno). Se aplican al elemento inmediatamente anterior: a+ coincide con "a", "aa", "aaa", etc. Para aplicar un cuantificador a una secuencia, usa paréntesis: (ab)+ coincide con "ab", "abab", "ababab".
Los cuantificadores con llaves permiten especificar cantidades exactas: {3} significa exactamente 3, {2,5} significa entre 2 y 5, {3,} significa 3 o más. Ejemplo: \d{4} coincide con exactamente 4 dígitos (útil para años). \d{1,3} coincide con 1 a 3 dígitos (útil para octetos de IPs).
Por defecto, los cuantificadores son codiciosos (greedy): consumen todo lo posible. El patrón .* en "<b>uno</b> <b>dos</b>" coincide con "b>uno</b> <b>dos</b" — todo desde la primera b hasta la última. Añadir ? después del cuantificador lo hace perezoso (lazy): .*? coincide con lo mínimo posible. En el mismo ejemplo, <b>.*?</b> coincide solo con "<b>uno</b>".
El rendimiento de los cuantificadores importa. Patrones como (a+)+ o (a|a)* pueden causar catastrophic backtracking con entradas que no coinciden. El motor prueba todas las combinaciones posibles antes de rendirse, resultando en tiempo exponencial. La solución es usar cuantificadores posesivos (a++ en PCRE) o atomic groups ((?>...)) que no permiten backtracking.
Cuantificadores:
* → Cero o más (greedy)
+ → Uno o más (greedy)
? → Cero o uno (greedy)
{n} → Exactamente n veces
{n,} → n o más veces
{n,m} → Entre n y m veces
Versiones lazy (mínimo posible):
*? → Cero o más (lazy)
+? → Uno o más (lazy)
?? → Cero o uno (lazy)
Ejemplo práctico:
/".*"/ sobre "uno" y "dos" → "uno" y "dos"
/".*?"/ sobre "uno" y "dos" → "uno"Anclas y límites
Las anclas no coinciden con caracteres, sino con posiciones en el texto. Las dos más comunes son ^ (inicio de línea/cadena) y $ (fin de línea/cadena). El patrón ^Hola coincide solo si "Hola" está al principio. Error$ coincide solo si "Error" está al final. Para que ^ y $ coincidan con inicio/fin de cada línea (no solo de la cadena completa), activa el flag multiline (m).
El límite de palabra \b es una de las anclas más útiles y menos comprendidas. Coincide con la posición entre un carácter de palabra (\w) y uno que no lo es (\W), o entre un carácter de palabra y el inicio/fin de la cadena. El patrón \bgato\b coincide con "gato" en "el gato negro" pero no en "gatonegro" ni en "atergatonado". Es ideal para buscar palabras completas sin preocuparse por la puntuación.
La aserción \A coincide solo con el inicio absoluto de la cadena (ignora el flag multiline), y \Z coincide con el final absoluto. En JavaScript no existen \A ni \Z, pero en Python, Ruby y PCRE son muy útiles cuando necesitas asegurar que el patrón valida la cadena completa sin importar la configuración de flags.
Un patrón de validación debería casi siempre usar ^...$ para asegurar que la cadena completa coincide, no solo una parte. Sin anclas, el patrón \d{4} coincide con "abc1234xyz" (los 4 dígitos centrales). Con anclas, ^\d{4}$ solo coincide si la cadena entera son exactamente 4 dígitos.
Grupos de captura y referencias
Los paréntesis () tienen doble función: agrupan elementos para aplicar cuantificadores y capturan el texto coincidente para referencia posterior. En el patrón (\d{2})/(\d{2})/(\d{4}) aplicado a "25/12/2026", el grupo 1 captura "25", el grupo 2 captura "12" y el grupo 3 captura "2026". En JavaScript, accedes a ellos con match[1], match[2], etc.
Los grupos con nombre (?<nombre>...) hacen el código más legible: (?<dia>\d{2})/(?<mes>\d{2})/(?<anio>\d{4}) permite acceder a los valores como match.groups.dia, match.groups.mes, match.groups.anio. Esto es mucho más mantenible que recordar números de grupo, especialmente en patrones complejos con muchos grupos.
Las referencias hacia atrás (backreferences) permiten coincidir con el mismo texto que un grupo capturó previamente. El patrón (\w+)\s+\1 coincide con palabras repetidas como "la la" o "el el" — \1 se refiere al texto exacto capturado por el primer grupo. Con grupos nombrados, usa \k<nombre>.
Los grupos no capturantes (?:...) agrupan sin capturar — útiles cuando necesitas agrupar para un cuantificador pero no te interesa extraer el valor. Ejemplo: (?:https?://)?(www\.)?ejemplo\.com usa un grupo no capturante para el protocolo opcional y solo captura el "www." si existe. Esto mejora el rendimiento y mantiene limpios los índices de captura.
// Grupos de captura en JavaScript
const fecha = "25/12/2026";
const regex = /(?<dia>\d{2})\/(?<mes>\d{2})\/(?<anio>\d{4})/;
const match = fecha.match(regex);
console.log(match.groups.dia); // "25"
console.log(match.groups.mes); // "12"
console.log(match.groups.anio); // "2026"
// Backreference: detectar palabras duplicadas
const texto = "El el gato se se fue";
const duplicadas = /\b(\w+)\s+\1\b/gi;
console.log(texto.match(duplicadas)); // ["El el", "se se"]
// Reemplazo con grupos capturados
const iso = fecha.replace(regex, '$<anio>-$<mes>-$<dia>');
console.log(iso); // "2026-12-25"Lookahead y lookbehind
Las aserciones lookahead y lookbehind (en conjunto "lookaround") comprueban si algo existe antes o después de la posición actual sin consumir caracteres. Son de ancho cero: no mueven el cursor de coincidencia. Esto las hace perfectas para validaciones complejas donde necesitas verificar múltiples condiciones en la misma posición.
Lookahead positivo (?=...): coincide si lo que sigue cumple el patrón. Ejemplo: \d+(?=€) coincide con dígitos seguidos de € sin incluir el € en la coincidencia. Lookahead negativo (?!...): coincide si lo que sigue NO cumple el patrón. Ejemplo: \d+(?!€) coincide con dígitos que NO están seguidos de €.
Lookbehind positivo (?<=...): coincide si lo que precede cumple el patrón. Ejemplo: (?<=€)\d+ coincide con dígitos precedidos de €. Lookbehind negativo (?<!...): coincide si lo que precede NO cumple el patrón. Nota: en JavaScript, lookbehind se soporta desde ES2018 (Chrome 62+, Node 8.10+). Algunos motores requieren lookbehind de longitud fija.
Caso de uso real: validar contraseñas con múltiples requisitos. El patrón ^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!@#$%]).{8,}$ usa cuatro lookaheads positivos en la posición inicial para verificar que existen mayúsculas, minúsculas, dígitos y caracteres especiales, sin importar en qué orden aparecen. Después, .{8,} verifica la longitud mínima.
// Lookahead: encontrar precios sin capturar el símbolo
const texto = "Cuesta 150€ o 200€ con envío";
const precios = texto.match(/\d+(?=€)/g);
console.log(precios); // ["150", "200"]
// Lookbehind: encontrar valores después de "Total:"
const factura = "Subtotal: 100, IVA: 21, Total: 121";
const total = factura.match(/(?<=Total:\s*)\d+/);
console.log(total[0]); // "121"
// Validación de contraseña con múltiples lookaheads
const passwordRegex = /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!@#$%&*]).{8,}$/;
console.log(passwordRegex.test("MiClave123!")); // true
console.log(passwordRegex.test("soloMinusculas")); // falsePatrones prácticos para el día a día
Email (simplificado): ^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$ — cubre la gran mayoría de direcciones válidas. La validación perfecta de email según RFC 5322 es extremadamente compleja (miles de caracteres de regex) y raramente necesaria. Para producción, valida el formato básico con regex y confirma con un email de verificación.
URL: ^https?:\/\/[\w.-]+(?:\.[a-zA-Z]{2,})(?:\/[^\s]*)?$ — coincide con URLs HTTP/HTTPS básicas. Para URLs complejas con query strings y fragmentos, considera usar el constructor URL() del navegador en lugar de regex. La especificación completa de URIs (RFC 3986) es difícil de capturar correctamente con una sola expresión regular.
Dirección IPv4: ^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$ — valida que cada octeto esté entre 0 y 255. Un error común es usar \d{1,3} sin validar el rango, lo que aceptaría "999.999.999.999". La versión con validación de rango es más larga pero correcta.
Número de teléfono español: ^(?:\+34)?[6-9]\d{8}$ — números españoles: opcionalmente +34, seguido de un dígito del 6 al 9 (móviles empiezan por 6-7, fijos por 8-9), seguido de 8 dígitos. Para formatos internacionales variados, librerías como libphonenumber son más fiables que regex por la complejidad de reglas por país.
// Patrones de uso diario
const patrones = {
email: /^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$/,
ipv4: /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/,
telefonoES: /^(?:\+34)?[6-9]\d{8}$/,
codigoPostalES: /^\d{5}$/,
fechaISO: /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/,
hexColor: /^#(?:[0-9a-fA-F]{3}){1,2}$/,
slugURL: /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
};
// Extraer todos los links de un HTML
const html = '<a href="https://ejemplo.com">Link</a>';
const links = html.match(/href="(.*?)"/g);
// Reemplazar múltiples espacios por uno
const limpio = " demasiados espacios ".replace(/\s+/g, " ").trim();
console.log(limpio); // "demasiados espacios"Rendimiento y errores que evitar
El catastrophic backtracking es el error de rendimiento más peligroso en regex. Ocurre cuando un patrón ambiguo se aplica a una entrada que no coincide: el motor prueba exponencialmente todas las combinaciones antes de declarar fallo. Ejemplo clásico: (a+)+ aplicado a "aaaaaaaaaaaaaaaaX" — el motor necesita probar 2^n combinaciones de cómo distribuir las "a" entre los dos cuantificadores anidados.
Para evitar problemas de rendimiento: no anides cuantificadores sobre el mismo conjunto de caracteres, usa cuantificadores posesivos o atomic groups cuando tu motor los soporte, y prefiere patrones específicos sobre genéricos. En lugar de .*texto.*, usa [^\n]*texto[^\n]* — el negated character class no puede coincidir con lo mismo que el texto buscado, eliminando la ambigüedad.
Otro error frecuente es olvidar escapar metacaracteres en texto literal. Si buscas "precio: 9.99€", el punto sin escapar coincide con cualquier carácter: "precio: 9X99€" también coincidiría. Usa \. para un punto literal. En JavaScript, si construyes regex desde strings de usuario, usa una función de escape: str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").
Finalmente, no intentes parsear HTML o XML con regex. Estos lenguajes tienen gramáticas anidadas recursivamente que las expresiones regulares no pueden manejar correctamente. Un patrón que funcione para <div>texto</div> fallará con <div><div>anidado</div></div>. Usa un parser DOM real (DOMParser en el navegador, cheerio en Node.js) para cualquier manipulación seria de HTML.