Expresiones regulares en PHP: preg_match, preg_replace y UTF-8

Expresiones regulares en PHP: las funciones preg_* usan el motor PCRE, el mismo estilo de patrones que casi todos los lenguajes. Lo propio de PHP está en los delimitadores, en el modificador u para el español y en cómo tratar el texto que escribe el usuario.

¿Coincide? preg_match

<?php
declare(strict_types=1);

$codigo = 'MX-2026-00418';

if (preg_match('/^([A-Z]{2})-(\d{4})-(\d{5})$/', $codigo, $m) === 1) {
    echo "País: $m[1], año: $m[2], número: $m[3]\n";
}

preg_match('/^(?<pais>[A-Z]{2})-(?<anio>\d{4})-(?<num>\d{5})$/', $codigo, $m);
echo "País: {$m['pais']}, año: {$m['anio']}\n";

var_dump(preg_match('/^\d{5}$/', '1234'));

Probar en el editor →

salidaPaís: MX, año: 2026, número: 00418
País: MX, año: 2026
int(0)

En PHP las expresiones regulares van entre delimitadores, normalmente barras (/…/), y se escriben entre comillas simples para que PHP no interprete las barras invertidas. preg_match devuelve 1 si hay coincidencia y 0 si no, y deja los grupos en el array del tercer argumento. Con (?<nombre>…) los grupos tienen nombre, mucho más legible que $m[3]. Si quieres probarlas antes, tenemos un probador de expresiones regulares.

Todas las coincidencias: preg_match_all

<?php
declare(strict_types=1);

$texto = 'Escríbenos a ventas@ejemplo.com o a soporte@ejemplo.org antes del 31/12/2026.';

preg_match_all('/[\w.+-]+@[\w-]+\.[\w.]+/u', $texto, $m);
print_r($m[0]);

preg_match_all('/(\d{2})\/(\d{2})\/(\d{4})/', $texto, $m, PREG_SET_ORDER);
foreach ($m as [$todo, $dia, $mes, $anio]) {
    echo "$todo → $anio-$mes-$dia\n";
}

Probar en el editor →

salidaArray
(
    [0] => ventas@ejemplo.com
    [1] => soporte@ejemplo.org
)
31/12/2026 → 2026-12-31

Reemplazar: preg_replace y preg_replace_callback

<?php
declare(strict_types=1);

echo preg_replace('/\s+/', ' ', "demasiados    espacios \n y saltos"), "\n";
echo preg_replace('/(\d{4})-(\d{2})-(\d{2})/', '$3/$2/$1', 'Entrega: 2026-10-27'), "\n";

$plantilla = 'Hola {nombre}, tu pedido {pedido} está listo.';
$datos = ['nombre' => 'Ana', 'pedido' => 'A-1042'];
echo preg_replace_callback('/\{(\w+)\}/', fn(array $m): string => $datos[$m[1]] ?? $m[0], $plantilla), "\n";

Probar en el editor →

salidademasiados espacios y saltos
Entrega: 27/10/2026
Hola Ana, tu pedido A-1042 está listo.

En el reemplazo, $1, $2… son los grupos. Cuando el reemplazo depende de lo encontrado, preg_replace_callback llama a una función por cada coincidencia; aquí rellena una plantilla sencilla.

Tildes y eñes: el modificador u

<?php
declare(strict_types=1);

$nombre = 'Ñandú';

var_dump(preg_match('/^\w+$/', $nombre));
var_dump(preg_match('/^\w+$/u', $nombre));
var_dump(preg_match('/^.{5}$/', $nombre));
var_dump(preg_match('/^.{5}$/u', $nombre));
print_r(preg_split('//u', 'año', -1, PREG_SPLIT_NO_EMPTY));

Probar en el editor →

salidaint(0)
int(1)
int(0)
int(1)
Array
(
    [0] => a
    [1] => ñ
    [2] => o
)

Sin el modificador u, el motor de expresiones regulares trabaja con bytes, y la ñ o la ú (dos bytes cada una) no cuentan como letras ni como un solo carácter. Con u, entiende UTF-8. Ponlo siempre que el texto lo escriba una persona.

Lo que escribe el usuario: preg_quote

<?php
declare(strict_types=1);

$busqueda = 'C++ (2026)';
$texto = 'Curso de C++ (2026) y de C# (2026)';

var_dump(preg_match('/' . $busqueda . '/', $texto));

$patron = '/' . preg_quote($busqueda, '/') . '/u';
echo $patron, "\n";
var_dump(preg_match($patron, $texto));

Probar en el editor →

salidaint(0)
/C\+\+ \(2026\)/u
int(1)

Si una expresión regular incluye texto del usuario, sus +, paréntesis o puntos se interpretan como operadores: aquí C++ significa «una o más C» y los paréntesis forman un grupo, así que el patrón no encuentra el texto que está ahí mismo. Con otros textos podría no compilar o, peor, coincidir con lo que no debe. preg_quote los escapa, y su segundo argumento es el delimitador que usas. Para buscar un texto literal sin más, ni siquiera hace falta una expresión regular: str_contains es más rápido y no tiene trampas.

Expresiones que tardan una eternidad. Patrones con repeticiones anidadas, como (a+)+, pueden tardar muchísimo con ciertos textos (se llama backtracking catastrófico). PHP corta la búsqueda al llegar a pcre.backtrack_limit y preg_match devuelve false: compara siempre con === 1 y revisa preg_last_error_msg() si algo no cuadra.

Resumen

ParaUsa
¿Coincide?preg_match($patron, $texto, $m) === 1
Todas las coincidenciaspreg_match_all (con PREG_SET_ORDER para iterar)
Reemplazarpreg_replace o preg_replace_callback
Partir un textopreg_split
Texto en españolel modificador u
Texto del usuario dentro del patrónpreg_quote($texto, '/')

Todo el código de esta página se ejecutó con PHP 8.5.12 en un contenedor limpio, sin acceso a la red, antes de publicarla; las salidas y los mensajes de error están copiados de esa ejecución.