Expresiones Regulares en PHP: Tips y Técnicas

Las expresiones regulares son una serie de carácteres que forman un patrón, normalmente representativo de otro grupo de carácteres mayor, de tal forma que podemos comparar el patrón con otro conjunto de carácteres para ver las coincidencias.

Las expresiones regulares estan disponibles en casi cualquier lenguaje de programación, pero aunque su sintaxis es relativamente uniforme, cada lenguaje usa su propio dialecto.

Una expresión regular es una expresión que describe un conjunto de cadenas sin enumerar sus elementos. Por ejemplo, el grupo formado por las cadenas Handel, Händel y Haendel se describe mediante el patrón "H(a|ä|ae)ndel". La mayoría de las formalizaciones proporcionan los siguientes constructores: una expresión regular es una forma de representar a los lenguajes regulares (finitos o infinitos) y se construye utilizando caracteres del alfabeto sobre el cual se define el lenguaje. Específicamente, las expresiones regulares se construyen utilizando los operadores unión, concatenación y clausura de Kleene.

Las expresiones regulares son la navaja suiza para buscar información a través de cierto patrones. Tienen un amplio arsenal de herramientas, algunas de las cuales a menudo no se conocen o no se utilizan correctamente. Hoy les voy a mostrar unas sugerencias para trabajar con expresiones regulares.

 

Agregando Comentarios

En muchos casos las expresiones regulares son bastante complejas e ilegibles. Una expresión regular que se el día de hoy, puede llegar a ser algo muy engorroso para entender el día de mañana a pesar de ser nuestro propio trabajo. Aprender a escribir escribir expresiones regulares es considerado por muchos como aprender un nuevo lenguaje de programación, son todo un mundo y al igual que la programación en general, es una buena idea tener mucho orden y añadir comentarios para mejorar la legibilidad de las expresiones regulares.

Por ejemplo, el siguiente es un patrón que podemos utilizar para comprobar los números de teléfono fijo de Lima, Perú y que incluya el código de país y departamento.

preg_match('/^(511[-\\s.])?(\\()?\\d{3}(?(2)\\))[-\\s.]?\\d{3}[-\\s.]?\\d{4}$/', $subject);

Esto podría ser mucho más legible si se agrega algunos comentarios y espacios extra:

preg_match("/^
            (511[-\s.])?	# opcional '511-', '511.' o '511'
            ( \( )?		# opcional abrir paréntesis
            \d{3}		# primeros 3 dígitos
            (?(2) \) )		# si hay un paréntesis abierto, ciérralo
            [-\s.]?		# seguido de un '-' o '.' o espacio
            \d{3}		# siguientes 3 dígitos
            [-\s.]?		# seguido de un '-' o '.' o espacio
            \d{4}		# últimos 4 dígitos
 
            $/x", $subject);

Vamos a probar el código anterior:

$telefonos = array(
"511 333 9912",
"511-333-9912",
"511.333.9912",
"(511) 333 9912",
"(511)-333-9912",
"(511).333.9912",
"511 33 4444");
 
foreach ($telefonos as $telefono)
{
	echo "$telefono es ";
 
	if (preg_match("/^
			(1[-\s.])?	# opcional '511-', '511.' o '511'
			( \( )?		# opcional abrir paréntesis
			\d{3}		# primeros 3 dígitos
			(?(2) \) )	# si hay un paréntesis abierto, ciérralo
			[-\s.]?		# seguido de un '-' o '.' o espacio
			\d{3}		# siguientes 3 dígitos
			[-\s.]?		# seguido de un '-' o '.' o espacio
			\d{4}		# últimos 4 dígitos
 
			$/x", $subject))
	{
		echo "válido\n";
	}
	else
	{
		echo "inválido\n";
	}
}
 
/*
resultados:
511 333 9912 es válido
511-333-9912 es válido
511.333.9912 es válido
(511) 333 9912 es válido
(511)-333-9912 es válido
(511).333.9912 es válido
511 33 4444 es inválido
*/

El truco consiste en usar el modificador 'x' al final de la expresión regular, esto hace que los espacios en blanco en el patrón sean ignorados, a menos que se escapen usando "\s". De esta forma será sencillo añadir comentarios. Los comentarios comienzan con "#" y terminan al final de la línea.

 

Usando Callbacks

En PHP se puede usar la función preg_replace_callback() para agregar una cierta funcionalidad al terminar de ejecutar las sustituciones con la expresión regular que esta siendo ejecutada.

A veces es necesario realizar sustituciones múltiples. Si se invoca a preg_replace() o str_replace() para cada patrón, la cadena será analizada una y otra vez.

Echemos un vistazo a este ejemplo, donde tenemos una plantilla de correo electrónico.

$template = "Hello [first_name] [last_name],
 
Thank you for purchasing [product_name] from [store_name].
 
The total cost of your purchase was [product_price] plus [ship_price] for shipping.
 
You can expect your product to arrive in [ship_days_min] to [ship_days_max] business days.
 
Sincerely,
[store_manager_name]";
 
// Asumiendo que el array $data tiene toda la información a reemplazar
// como $data['first_name'] $data['product_price'] etc...
 
$template = str_replace("[first_name]", $data['first_name'], $template);
$template = str_replace("[last_name]", $data['last_name'], $template);
$template = str_replace("[store_name]", $data['store_name'], $template);
$template = str_replace("[product_name]", $data['product_name'], $template);
$template = str_replace("[product_price]", $data['product_price'], $template);
$template = str_replace("[ship_price]", $data['ship_price'], $template);
$template = str_replace("[ship_days_min]", $data['ship_days_min'], $template);
$template = str_replace("[ship_days_max]", $data['ship_days_max'], $template);
$template = str_replace("[store_manager_name]", $data['store_manager_name'], $template);
 
// esto también pudo haber sido un bucle,
// pero lo que se quería era hacer énfasis en cuantos reemplazos se realizaron

Observe que cada sustitución tiene algo en común. siempre son cadenas encerradas entre corchetes, podríamos coger todos los datos con una sola expresión regular y manejar los reemplazos en una función callback.

Así que esta es mejor manera de hacer este reemplazo utilizando funciones callback:

// Esto llamará a myCallback() siempre que este entre corchetes
$template = preg_replace_callback('/\[(.*)\]/', 'myCallback', $template);
 
function myCallback($matches)
{
	// $matches[1] ahora contiene la cadena entre los corchetes
	if (isset($data[$matches[1]]))
	{
		// retorna la cadena de reemplazo
		return $data[$matches[1]];
	}
	else
	{
		return $matches[0];
	}
}

Ahora la cadena en $template solo será analizada por la expresión regular una vez.

 

Greedy vs. Ungreedy

Antes de empezar a explicar este concepto, mostraré un ejemplo para tratar de llegar a una idea ya que desconozco la tradución de estos términos. Digamos que estamos buscando anchor tags en un fuente html:

$html = 'Hello <a href="/world">World!</a>';
 
if (preg_match_all('/.*<\/a>/', $html, $matches))
{
	print_r($matches);
}

El resultado será tal como se espera:

/*
output:
Array
(
    [0] => Array
        (
            [0] => <a href="/world">World!</a>
        )
 
)
*/

Vamos a cambiar el input y a añadir un segundo anchor tag:

$html = '<a href="/hello">Hello</a>
<a href="/world">World!</a>';
 
if (preg_match_all('/.*<\/a>/', $html, $matches))
{
	print_r($matches);
}
 
/*
output:
Array
(
    [0] => Array
        (
            [0] => <a href="/hello">Hello</a>
            [1] => <a href="/world">World!</a>
 
        )
 
)
*/

Una vez más parece estar bien... hasta ahora. Pero en realidad la única razón por la que esto funciona es porque los anchor tags estan en líneas separadas, y por defecto PCRE compara patrones de solo una línea a la vez. Si nos encontramos con dos anchor tags en la misma línea, esto ya no funcionará como esperabamos:

$html = '<a href="/hello">Hello</a> <a href="/world">World!</a>';
 
if (preg_match_all('/.*<\/a>/', $html, $matches))
{
	print_r($matches);
}
 
/*
output:
Array
(
    [0] => Array
        (
            [0] => <a href="/hello">Hello</a> <a href="/world">World!</a>
 
        )
 
)
*/

Esta vez el patrón coincide con la primera etiqueta de apertura, y la última etiqueta de cierre y todo entre estas como una única coincidencia, en lugar de mantenerlas por separado como dos coincidencias. Esto se debe al comportamiento predeterminado llamado "greedy". Que vendría a ser algo así como que por defecto toman todo lo que puedan como única coincidencia, eso pasó en este último caso, que en lugar de obtener dos resultados se obtuvo uno solo con ambas coincidencias.

Si se agrega un signo de interrogación después del cuantificador (.*?) se convertirá en "ungreedy".

$html = '<a href="/hello">Hello</a> <a href="/world">World!</a>';
 
if (preg_match_all('/.*?<\/a>/', $html, $matches))
{
	print_r($matches);
}
 
/*
output:
Array
(
    [0] => Array
        (
            [0] => <a href="/hello">Hello</a> [1] => <a href="/world">World!</a> )
 
)
*/

Ahora el resultado es correcto. Otra forma de provocar el comportamiento "ungreedy" es utilizar el modificador U en el patrón.

 

Condicionales

Las expresiones regulares proporcionan la funcionalidad para controlar condiciones. El formato es el siguiente:

(?(condition) true-pattern | false-pattern)
o
(?(condition) true-pattern)

La condición puede ser un número. En este caso se refiere a un sub-patrón previamente capturado.
Por ejemplo, podemos usar esto para verificar la apertura y cierre de los tags.

$regex = '/^(<)?[a-z]+(?(1)>)$/';
 
preg_match($regex, ''); // true
preg_match($regex, ''); // false
preg_match($regex, 'hello'); // true

En el ejemplo anterior "1" se refiere al sub-patrón "<"  que también es opcional ya que está seguido por un signo de interrogación. Sólo si la condición es verdadera, coincide con un paréntesis de cierre.
La condición también puede ser una afirmación:

// si empieza con 'q', debería continuar con 'qu'
// sino debería empezar con 'f'
$regex = '/^(?(?=q)qu|f)/';
 
preg_match($regex, 'quake'); // true
preg_match($regex, 'qwerty'); // false
preg_match($regex, 'foo'); // true
preg_match($regex, 'bar'); // false

 

Filtrado de patrones

Hay varias razones para filtrar los inputs en el desarrollo de aplicaciones web. Siempre se debría filtrar la data antes de insertarla en la base de datos o mostrarla en el navegador. Así mismo, es necesario filtrar cualquier cadena antes de incluirla en una expresión regular. PHP provee una función llamada preg_quote para hacer este trabajo.

En el siguiente ejemplo se utiliza una cadena que contiene un carácter especial "*".

$word = '*world*';
 
$text = 'Hello *world*!';
 
preg_match('/' . $word . '/', $text); // muestra una advertencia
preg_match('/' . preg_quote($word) . '/', $text); // true

Lo mismo se puede realizar también encerrando la cadena entre \Q y \E. Cualquier carácter especial después de \Q será ignorado hasta llegar al \E.

$word = '*world*';
 
$text = 'Hello *world*!';
 
preg_match('/\Q' . $word . '\E/', $text); // true

Sin embargo, este segundo método no es 100% seguro ya que la misma cadena podría contener un \E.

 

Evitar la captura de sub-patrones

Los sub-patrones, encerrados entre paréntesis, son capturados en un array para que puedan ser usados posteriormente. Pero también existe una forma de evitar que sean capturados.

Comencemos con un ejemplo muy sencillo:

preg_match('/(f.*)(b.*)/', 'Hello foobar', $matches);
 
echo "f* => " . $matches[1];
echo "b* => " . $matches[2];
 
/*
output:
'f* => foo'
'b* => bar'
*/

Ahora vamos a hacer un pequeño cambio agregando otro sub-patrón (H.*) antes de todo lo anterior:

preg_match('/(H.*) (f.*)(b.*)/', 'Hello foobar', $matches);
 
echo "f* => " . $matches[1];
echo "b* => " . $matches[2];
/*
output:
'f* => Hello'
'b* => foo'
*/

El array $matches ha cambiado, lo cual podría causar que el script deje de funcionar correctamente, dependiendo de lo que se haga en el código. Ahora tenemos que encontrar cada ocurrencia del array $matches en el código y ajustar el número de índice correctamente.

Si no se está interesado en el contenido del nuevo sub-patrón que se acaba de agregar, podría marcarlo como de "no captura" de la siguiente forma:

preg_match('/(?:H.*) (f.*)(b.*)/', 'Hello foobar', $matches);
 
echo "f* => " . $matches[1];
echo "b* => " . $matches[2];
 
/*
output:
'f* => foo'
'b* => bar'
*/

Al agregar "?:" al inicio del sub-patrón este no sera capturado en el array $matches, por lo que los valores del array no cambiaran.

 

Agregar un nombre a los sub-patrones

Existe otro método para prevenir lo que paso inicialmente en el ejemplo anterior. Se podría agregar un nombre a cada sub-patrón, de modo que posteriormente se podría hacer referencia a estos usando los nombres en lugar de los índices del array. El formato será (?Ppatron)

El primer ejemplo de la sección anterior se podría reescribir de la siguiente manera:

preg_match('/(?P<fstar>f.*)(?P<bstar>b.*)/',  'Hello foobar', $matches);
 
echo "f* => " . $matches['fstar'];
echo "b* => " . $matches['bstar'];
/*
output:
'f* => foo'
'b* => bar'
*/

Ahora se puede agregar otro sub-patrón sin molestar las coincidencias existentes en el array $matches:

preg_match('/(?P<hi>H.*) (?P<fstar>f.*)(?P<bstar>b.*)/', 'Hello foobar', $matches);
 
echo "f* => " . $matches['fstar'];
echo "b* => " . $matches['bstar'];
echo "h* => " . $matches['hi'];
 
/*
output:
'f* => foo'
'b* => bar'
'h* => Hello'
*/

Trackback URL for this post:

http://shadowjah.com/trackback/expresiones_regulares_php_tips_tecnicas