Regex avanzada en PHP: grupos con nombre, lookahead y lookbehind

Una vez dominados los grupos básicos y los cuantificadores, las regex en PHP ofrecen construcciones más potentes: grupos con nombre para capturas legibles, non-capturing groups para agrupar sin capturar, lookahead y lookbehind para condiciones de contexto sin consumir caracteres, y el modificador x para escribir patrones comentados y mantenibles.

Grupos con nombre: (?P<nombre>...)

Ya vistos brevemente, los grupos con nombre hacen el código mucho más legible que los índices numéricos:

<?php
// Parsear una URL de forma estructurada
$url = 'https://api.ejemplo.com:8080/v2/usuarios?page=2&limit=50#resultados';

preg_match(
    '/^(?P<scheme>https?)://(?P<host>[w.-]+)(?::(?P<port>d+))?(?P<path>/[^?#]*)?(?:?(?P<query>[^#]*))?(?:#(?P<fragment>.*))?$/',
    $url,
    $m
);

echo $m['scheme'];   // https
echo $m['host'];     // api.ejemplo.com
echo $m['port'];     // 8080
echo $m['path'];     // /v2/usuarios
echo $m['query'];    // page=2&limit=50
echo $m['fragment']; // resultados
?>

Non-capturing groups: (?:...)

(?:...) agrupa sin crear una captura, útil cuando necesitas agrupar para un cuantificador pero no te interesa el valor capturado:

<?php
// Con grupo capturador: $m[1] contiene el prefijo (no lo queremos)
preg_match('/^(https?|ftp)://(.+)$/', 'https://php.net', $m);
echo $m[1]; // https   ? grupo innecesario
echo $m[2]; // php.net

// Con non-capturing group: $m[1] es directamente el host
preg_match('/^(?:https?|ftp)://(.+)$/', 'https://php.net', $m);
echo $m[1]; // php.net ? el único grupo que nos interesa

// Útil con cuantificadores
preg_match_all('/(?:ab)+/', 'ababab', $m);
echo $m[0][0]; // ababab ? captura la repetición completa, no el último "ab"
?>

Lookahead positivo: (?=...)

Un lookahead positivo afirma que lo que sigue al punto de coincidencia es un patrón concreto, pero sin consumirlo (no se incluye en la coincidencia ni en la posición):

<?php
// Extraer números que van seguidos de " EUR" sin incluir " EUR"
$texto = "Precio: 25 EUR. Descuento: 5 EUR. Total: 20 EUR.";
preg_match_all('/d+(?= EUR)/', $texto, $m);
print_r($m[0]); // ['25', '5', '20']

// Validar contraseña: al menos una mayúscula, un dígito y 8 chars mínimo
function validarPassword(string $pass): bool
{
    return preg_match(
        '/^(?=.*[A-Z])(?=.*d)(?=.*[^A-Za-z0-9]).{8,}$/',
        $pass
    ) === 1;
}
var_dump(validarPassword('MiPass1!'));   // true
var_dump(validarPassword('sindigito!')); // false
?>

Lookahead negativo: (?!...)

<?php
// Extraer "php" que NO va seguido de ".net" o ".org"
$texto = 'php php.net php.org php-src';
preg_match_all('/php(?!.net|.org)/', $texto, $m);
print_r($m[0]); // ['php', 'php']   ? coincide con "php" y "php" en "php-src"
?>

Lookbehind positivo: (?<=...)

<?php
// Extraer números precedidos de "$" sin incluir "$"
$precios = 'Total: $25.00, IVA: $5.25, Final: $30.25';
preg_match_all('/(?<=$)d+.d+/', $precios, $m);
print_r($m[0]); // ['25.00', '5.25', '30.25']
?>

Lookbehind negativo: (?<!...)

<?php
// Encontrar "log" que NO esté precedido de "error_"
$texto = 'log activo, error_log, debug_log, warning_log';
preg_match_all('/(?<!error_)blogb/', $texto, $m);
print_r($m[0]); // ['log']  ? solo el primero; debug_log y warning_log también coinciden
?>

Modificador x: regex comentada y legible

<?php
// Sin /x: ilegible para una fecha ISO con hora opcional
preg_match('/^(d{4})-(d{2})-(d{2})(?:T(d{2}):(d{2})(?::(d{2}))?)?$/', $str, $m);

// Con /x: espacios y comentarios para documentar el patrón
$patron = '/
    ^                     # inicio
    (d{4})               # año
    -
    (d{2})               # mes
    -
    (d{2})               # día
    (?:                   # parte de hora (opcional)
        T
        (d{2})           # hora
        :
        (d{2})           # minutos
        (?::(d{2}))?     # segundos (opcionales)
    )?
    $                     # fin
/x';
preg_match($patron, '2026-07-21T14:30:00', $m);
echo $m[4]; // 14 (hora)
?>

Possessive quantifiers y atomic groups

<?php
// Cuantificador posesivo ++: no hace backtracking
// Útil para evitar catastrophic backtracking en patrones con alternativas
// PCRE soporta ++ pero no todos los sabores de regex lo tienen

// En lugar de /(w+s+)*/  que puede hacer backtracking exponencial,
// usa /(?:w++s++)*/ con possessive quantifiers para forzar que el motor
// no retroceda una vez que ha consumido caracteres

// Parsear líneas de log sin backtracking peligroso
$log = '2026-07-21 14:30:00 ERROR conexión rechazada';
preg_match('/^(d{4}-d{2}-d{2})++ ++(d{2}:d{2}:d{2})++ ++(w++)++ ++(.++)$/', $log, $m);
echo $m[3]; // ERROR
echo $m[4]; // conexión rechazada
?>

La referencia de patrones PCRE en PHP cubre en detalle los lookaheads/lookbehinds de longitud variable (disponibles desde PCRE2), los atomic groups y los possessive quantifiers con su impacto en el rendimiento.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP