Introducción
Las expresiones regulares — comúnmente llamadas regex o regexp — son secuencias de caracteres que definen un patrón de búsqueda. Son una de las herramientas más poderosas en el arsenal de cualquier desarrollador, permitiendo buscar, validar, extraer y transformar texto con una sola expresión compacta.
Ya sea validando una dirección de correo electrónico en un formulario web, extrayendo datos de registros de logs, o realizando un complejo buscar-y-reemplazar en miles de archivos, las expresiones regulares te permiten expresar exactamente lo que buscas en una notación concisa y portable que entiende prácticamente cualquier lenguaje de programación y la mayoría de los editores de texto.
Breve Historia de las Expresiones Regulares
La historia de las expresiones regulares se remonta a los fundamentos de la ciencia de la computación teórica:
- 1951 — El matemático Stephen Kleene formaliza el concepto de lenguajes regulares e introduce la notación de la estrella de Kleene (
*) como parte de su trabajo en teoría de autómatas. - 1968 — Ken Thompson implementa expresiones regulares en el editor de texto QED y luego en herramientas Unix como
grep,sedyawk. - 1986 — POSIX estandariza dos variantes: BRE (Expresiones Regulares Básicas) y ERE (Expresiones Regulares Extendidas).
- 1997 — Philip Hazel crea la librería PCRE (Perl Compatible Regular Expressions), que se convierte en el estándar de facto para regex avanzado.
- 1999 — ECMAScript 3 estandariza el objeto
RegExpde JavaScript. - 2015 — ES6 añade las banderas
u(Unicode) ey(sticky). - 2018 — ES2018 añade grupos de captura nombrados (
(?<nombre>...)) y aserciones lookbehind.
POSIX vs PCRE vs JavaScript RegExp
| Característica | BRE/ERE (POSIX) | PCRE | JavaScript RegExp |
|---|---|---|---|
| Lookahead | ✗ | ✓ | ✓ |
| Lookbehind | ✗ | ✓ | ✓ (ES2018+) |
| Grupos nombrados | ✗ | ✓ | ✓ (ES2018+) |
| Coincidencia no voraz | ✗ | ✓ | ✓ |
| Unicode | Limitado | ✓ | ✓ (con bandera u) |
| Retroreferencias | ✓ | ✓ | ✓ |
Referencia de Sintaxis Core
Tabla de Referencia Rápida
| Patrón | Significado |
|---|---|
. |
Cualquier carácter excepto salto de línea |
^ |
Inicio de cadena / inicio de línea (con bandera m) |
$ |
Fin de cadena / fin de línea (con bandera m) |
\d |
Cualquier dígito [0-9] |
\D |
Cualquier no-dígito |
\w |
Carácter de palabra [a-zA-Z0-9_] |
\W |
Carácter de no-palabra |
\s |
Espacio en blanco (espacio, tabulador, salto de línea…) |
\S |
Carácter que no es espacio en blanco |
[abc] |
Clase de caracteres — coincide con a, b o c |
[^abc] |
Clase negada — coincide con cualquier cosa excepto a, b, c |
[a-z] |
Rango de caracteres |
* |
0 o más veces (voraz) |
+ |
1 o más veces (voraz) |
? |
0 o 1 vez (voraz) |
{n} |
Exactamente n veces |
{n,m} |
Entre n y m veces (voraz) |
*? +? ?? |
Equivalentes perezosos (no voraces) |
(abc) |
Grupo de captura |
(?:abc) |
Grupo de no-captura |
(?<nombre>abc) |
Grupo de captura nombrado |
| |
Alternancia — coincide con izquierda O derecha |
(?=...) |
Lookahead positivo |
(?!...) |
Lookahead negativo |
(?<=...) |
Lookbehind positivo |
(?<!...) |
Lookbehind negativo |
Clases de Caracteres
Las clases de caracteres permiten coincidir con un carácter de un conjunto. [aeiou] coincide con cualquier vocal. [a-zA-Z] coincide con cualquier letra. [^0-9] coincide con cualquier carácter que no sea un dígito.
Las clases abreviadas más comunes:
\dequivale a[0-9]\wequivale a[a-zA-Z0-9_]\scoincide con espacio, tabulador (\t), salto de línea (\n), retorno de carro (\r) y otros espacios en blanco
Cuantificadores: Voraces vs Perezosos
Por defecto, los cuantificadores son voraces — coinciden con tanto como sea posible. Considera la cadena HTML <b>negrita</b> y <i>cursiva</i>:
<.*> → coincide con toda la cadena desde <b> hasta </i> (voraz)
<.*?> → coincide con <b>, luego </b>, luego <i>, luego </i> (perezoso)
Añadir ? después de un cuantificador lo hace perezoso (no voraz): coincide con lo mínimo posible mientras el patrón global siga siendo exitoso.
Anclas
^coincide con el inicio de la cadena (o de la línea con la banderam).$coincide con el final de la cadena (o de la línea conm).\bcoincide con un límite de palabra — la transición entre un carácter de palabra y uno que no lo es.\Bcoincide con un no-límite de palabra.
Grupos y Retroreferencias
Los grupos de captura (...) capturan el texto coincidente, al que puedes referirte después como \1, \2, etc. (retroreferencias), o acceder a través del array de resultados.
Los grupos de no-captura (?:...) agrupan subpatrones sin crear una captura, lo cual es más eficiente cuando no necesitas el valor capturado.
Los grupos nombrados (?<año>\d{4}) permiten referenciar capturas por nombre (match.groups.año en JavaScript), haciendo los patrones mucho más legibles.
Lookahead y Lookbehind
Estas aserciones de anchura cero coinciden con una posición, no con un carácter:
\d+(?= euros) → coincide con dígitos solo si van seguidos de " euros"
\d+(?! euros) → coincide con dígitos que NO van seguidos de " euros"
(?<=\$)\d+ → coincide con dígitos solo si están precedidos por "$"
(?<!\$)\d+ → coincide con dígitos que NO están precedidos por "$"
Los lookaheads y lookbehinds no consumen caracteres, por lo que el texto coincidente no incluye la parte del lookahead/lookbehind.
Banderas (Flags)
| Bandera | Nombre | Efecto |
|---|---|---|
i |
Sin distinción de mayúsculas | [a-z] también coincide con [A-Z] |
g |
Global | Encuentra todas las coincidencias, no solo la primera |
m |
Multilínea | ^ y $ coinciden con límites de línea |
s |
dotAll | . también coincide con saltos de línea |
u |
Unicode | Habilita coincidencia Unicode completa; requerido para \p{} |
y |
Sticky | Solo coincide en la posición lastIndex |
x |
Verboso/Extendido | Permite espacios en blanco y comentarios (solo PCRE/Python) |
La bandera x (verbosa) es especialmente valiosa para documentar patrones complejos:
import re
pattern = re.compile(r"""
^ # inicio de cadena
(?P<year>\d{4}) # año de 4 dígitos
-
(?P<month>0[1-9]|1[0-2]) # mes 01–12
-
(?P<day>0[1-9]|[12]\d|3[01]) # día 01–31
$
""", re.VERBOSE)
Patrones Comunes con Ejemplos Reales
Validación de Correo Electrónico
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
Desglose:
^[a-zA-Z0-9._%+-]+— parte local (letras, dígitos y caracteres especiales permitidos)@— arroba literal[a-zA-Z0-9.-]+— nombre de dominio\.[a-zA-Z]{2,}$— TLD de 2+ letras
Coincidencia de URL
https?://(?:www\.)?[a-zA-Z0-9-]+(?:\.[a-zA-Z]{2,})+(?:/[^\s]*)?
Fecha ISO (YYYY-MM-DD)
\b\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])\b
Valida mes 01–12 y día 01–31. No valida rangos de días específicos por mes (ej. el 30 de febrero pasaría la validación).
Dirección IPv4
\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b
Desglose:
25[0-5]— 250–2552[0-4]\d— 200–249[01]?\d\d?— 0–199
Número de Teléfono (EE. UU.)
\+?1?\s?[\(]?\d{3}[\)]?[-.\s]?\d{3}[-.\s]?\d{4}
Coincide con formatos como (555) 123-4567, 555-123-4567, +1 555 123 4567.
Código de Color Hexadecimal
#(?:[0-9A-Fa-f]{3}){1,2}\b
Coincide con colores hex de 3 dígitos (#F00) y 6 dígitos (#FF0000).
Expresiones Regulares en Diferentes Lenguajes de Programación
JavaScript
// Sintaxis literal con banderas
const regex = /^hello\s+world$/im;
const match = "Hello World".match(regex);
// Sintaxis de constructor (útil para patrones dinámicos)
const term = "world";
const dynamic = new RegExp(`hello\\s+${term}`, "im");
// Reemplazar todas las ocurrencias
const result = "foo bar foo".replaceAll(/foo/g, "baz");
// Capturas nombradas (ES2018+)
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const { year, month, day } = "2024-03-15".match(dateRegex).groups;
Python
import re
# Compilar para reutilización
pattern = re.compile(r'^hello\s+world$', re.IGNORECASE | re.MULTILINE)
match = pattern.match("Hello World")
# Encontrar todas las coincidencias
dates = re.findall(r'\d{4}-\d{2}-\d{2}', text)
# Sustituir con una función
result = re.sub(r'\b\d+\b', lambda m: str(int(m.group()) * 2), "1 más 2 es 3")
# Grupos nombrados
m = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})', "2024-03")
print(m.group('year')) # 2024
Java
import java.util.regex.*;
Pattern p = Pattern.compile("^hello\\s+world$",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
Matcher m = p.matcher("Hello World");
boolean found = m.matches();
// Extraer grupos
Pattern datePattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher dm = datePattern.matcher("Hoy es 2024-03-15");
if (dm.find()) {
String year = dm.group(1);
}
Go
import "regexp"
re := regexp.MustCompile(`(?im)^hello\s+world$`)
match := re.FindString("Hello World")
// Encontrar todas las sub-coincidencias
dateRe := regexp.MustCompile(`(\d{4})-(\d{2})-(\d{2})`)
all := dateRe.FindAllStringSubmatch(text, -1)
for _, m := range all {
year, month, day := m[1], m[2], m[3]
_ = year; _ = month; _ = day
}
// Grupos nombrados
namedRe := regexp.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})`)
match2 := namedRe.FindStringSubmatch("2024-03")
yearIdx := namedRe.SubexpIndex("year")
fmt.Println(match2[yearIdx]) // 2024
Rendimiento y Backtracking Catastrófico
Cómo Funciona el Backtracking
La mayoría de los motores de regex utilizan coincidencia basada en NFA (Autómata Finito No Determinista), lo que significa que pueden intentar múltiples caminos a través del patrón cuando un intento temprano falla. Este backtracking es lo que permite características como los lookaheads y las retroreferencias, pero también puede ser una trampa de rendimiento.
El Caso Catastrófico
Considera el patrón (a+)+ aplicado a la cadena "aaaaaX":
- El
+externo intenta coincidir con tantos grupos como sea posible. - Cuando el motor alcanza
Xy falla, hace backtracking e intenta diferentes formas de dividir los caracteresaentre las repeticiones del grupo. - Para una cadena de longitud n, hay 2^(n-1) divisiones posibles — llevando a una complejidad de tiempo exponencial.
(a+)+ sobre "aaaaaaaaaaaaaaaaaX" → ¡puede tardar segundos o minutos!
Otros patrones peligrosos incluyen (a|aa)+, (\w+\s*)+, y cualquier cosa con cuantificadores anidados sobre clases de caracteres superpuestas.
Cómo Evitarlo
- Evitar cuantificadores anidados sobre el mismo conjunto de caracteres:
(a+)+→ usara+en su lugar. - Usar grupos atómicos
(?>...)o cuantificadores posesivosa++(PCRE) para prevenir el backtracking en grupos ya coincidentes. - Ser específico: reemplazar
.*con una clase de caracteres que excluya delimitadores (ej.[^"]*dentro de cadenas entre comillas). - Anclar patrones donde sea posible para que el motor falle rápido.
- Usar un timeout en código de producción cuando se procesa entrada no confiable.
Cómo Leer una Expresión Regular Compleja
Desglosemos ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$:
^ → ancla: inicio de cadena
[a-zA-Z0-9._%+-]+ → uno o más caracteres permitidos en la parte local
@ → @ literal
[a-zA-Z0-9.-]+ → uno o más caracteres de dominio
\. → punto literal (escapado)
[a-zA-Z]{2,} → TLD: 2 o más letras
$ → ancla: fin de cadena
Consejo: Usa el probador de regex de este sitio para resaltar cada grupo capturado y ver exactamente qué parte de la entrada coincide con cada token. Construye tu patrón incrementalmente — empieza con la pieza válida más simple, verifícala, luego extiéndela.
Mejores Prácticas
- Compilar una vez, usar muchas veces. Pre-compilar un patrón (ej.
re.compile()en Python,Pattern.compile()en Java) es mucho más eficiente que re-parsearlo en cada llamada. - Preferir grupos de no-captura
(?:...)cuando no necesitas el valor capturado. Señala la intención y evita la asignación de memoria innecesaria. - Usar cadenas raw para los patrones. En Python, usa
r'\d+'en lugar de'\\d+'para evitar el doble escapado. En JavaScript, la sintaxis literal/\d+/lo maneja automáticamente. - Nombrar tus capturas.
(?<año>\d{4})es mucho más mantenible que depender del índice de grupo\1. - Probar con casos límite: cadenas vacías, cadenas que casi-pero-no-del-todo coinciden, caracteres Unicode y entradas muy largas.
- Documentar patrones complejos con la bandera
x(verbosa) en Python/PCRE, o con comentarios inline en tu código. - Nunca usar regex para parsear HTML o XML completos. Usa una librería de parser apropiada.
- Validar entradas en el lado del servidor. La validación regex del lado del cliente mejora la UX pero no debe ser la única línea de defensa.
Preguntas Frecuentes (FAQ)
P: ¿Cuál es la diferencia entre match() y search() en Python?
R: re.match() solo coincide al principio de la cadena. re.search() escanea toda la cadena buscando una coincidencia. Usa re.fullmatch() para requerir que el patrón coincida con toda la cadena.
P: ¿Por qué ^ dentro de una clase de caracteres [^abc] significa algo diferente?
R: Dentro de una clase de caracteres, ^ como primer carácter niega la clase — coincide con cualquier carácter que no esté en el conjunto. Fuera de una clase de caracteres, ^ es un ancla para el inicio de la cadena.
P: ¿Puedo usar regex para parsear HTML?
R: Para extracciones simples y bien definidas de estructuras HTML conocidas, regex puede funcionar. Pero HTML no es un lenguaje regular — permite anidamiento arbitrario y etiquetas de cierre opcionales. Usa un parser HTML apropiado (ej. BeautifulSoup en Python, DOMParser en JS) para un parseo robusto.
P: ¿Cuál es la diferencia entre cuantificadores voraces y posesivos?
R: Los cuantificadores voraces hacen backtracking — intentan la coincidencia máxima y devuelven caracteres si es necesario. Los cuantificadores posesivos (ej. a++ en PCRE) nunca devuelven — una vez que coinciden, la coincidencia queda bloqueada. Esto previene el backtracking catastrófico pero también puede causar que una coincidencia falle cuando un cuantificador voraz habría tenido éxito.
P: ¿Cómo hago coincidir un punto literal . o un paréntesis (?
R: Escápalos con una barra invertida: \. coincide con un punto literal, \( coincide con un paréntesis abierto literal.
P: ¿Es la regex sensible a mayúsculas por defecto?
R: Sí. Usa la bandera i (/patrón/i en JavaScript, re.IGNORECASE en Python) para habilitar la coincidencia insensible a mayúsculas.
P: ¿Qué coincide con \b?
R: \b es una aserción de límite de palabra de anchura cero. Coincide con la posición entre un carácter de palabra (\w) y un carácter de no-palabra (\W).
P: ¿Cómo pruebo si una cadena completa coincide con un patrón?
R: Ancla con ^ y $: ^patrón$. En Python, también puedes usar re.fullmatch(). En JavaScript, usa .test() con anclas o verifica que match()[0].length === input.length.
Usa el probador de regex de este sitio para experimentar con cada patrón de esta guía. Pega cualquier patrón, escribe tu cadena de prueba y observa las coincidencias resaltadas en tiempo real.