domingo, 1 de febrero de 2015

[Parte 6] Web Scraping

Scraping con expresiones regulares

Expresiones regulares: una introduccion rapida

Expresiones regulares o regex son la navaja suiza del procesamiento de texto y son una parte importante del web scraping. Una vez que se sienta comodo con simplehtmldom es cuando puede embarcarse al uso de las expresiones regulares en sus proyectos de scraping. Este es un tema complejo y se supone que tiene un conocimiento rudimentario de ello. No lo cubrimos en detalle aqui, pero puede ver mas acerca de ello aqui: http://arthusu.blogspot.mx/2013/07/phpexpresiones-regulares-pcre.html
Una rapida vision general de las expresiones regulares sin embargo se presenta en este capitulo.

Las expresiones regulares son una herramienta increiblemente poderosa disponible en la mayoria de los lenguajes de programacion de hoy. Piense en expresiones regulares como un sofisticado sistema que coincide con un patron de texto. Se especifica un patron y luego usa las funciones incorporadas de PHP para aplicar el patron en una cadena de texto para ver si coinciden. PHP es compatible con dos tipos de normas de expresiones regulares,POSIX Extended and Perl-Compatible (PCRE). PCRE es actualmente el tipo preferido para utilizar con
PHP, tiende a ser mas rapido que la opcion POSIX, y utiliza la misma sintaxis de expresiones regulares que PERL. Puede identificar facilmente estas funciones ya que comienzan con el prefijo preg. Ejemplos de funciones de expresiones regulares PCRE en PHP son preg_replace(), preg_split(), preg_match(), y preg_match_all(). Vamos a describir solo las expresiones regulares PCRE en esta parte, y por simplicidad, tambien vamos a limitar nuestra discusion a las funciones mas utilizadas dentro de PHP. Echemos un vistazo a la coincidencia de cadenas en PHP primero. La funcion de PHP a utilizar para expresiones regulares es preg_match(). Considere la cadena:



"las ideas verdes incoloras duermen furiosamente"

Vamos a utilizar la cadena anterior para hacer coincidir con patrones. Aqui esta el primer ejemplo:

$string = "las ideas verdes incoloras duermen furiosamente";
echo preg_match('/verdes/', $string) ; // retorna 1


Aqui estamos buscando el patron 'verdes' en cualquier lugar dentro de la cadena proporcionada. Cada vez que usted este buscando una cierta cadena o patron dentro de una cadena dada, hay que delimitar primero el patron. En general, usted hace esto con el caracter de barra diagonal (/), pero se puede utilizar cualquier otro caracter no alfanumerico. Por lo tanto si usted esta buscando un patron que concuerde con 'verdes' podria configurarlo con /verdes/ o #verdes#, siempre y cuando usted utilice los mismos caracteres en ambos extremos del patron y que no esta entre la cadena patron que esta buscando. Por ejemplo, los siguientes son equivalentes:

preg_match('/Hola/', 'Hola Mundo!'); // devuelve 1
preg_match('#Hola#', 'Hola Mundo!'); // devuelve 1


El formato general de la funcion preg_match() es la siguiente.

int preg_match ( string $pattern , string $subject [, array &$matches [, int $flags = 0 [, int $offset = 0 ]]] )
 
Podriamos haber logrado lo anterior con alguna de las funciones de cadena mas basicas que PHP proporciona, pero la funcion preg_match() tambien viene con algunos cuantificadores en el patron que aumentan el poder de las expresiones regulares, unos pocos seleccionados se muestran a continuacion.

Cuantificadores de patron

^ prueba para el patron al inicio de la cadena
$ prueba para el patron al final de la cadena
. coincide con cualquier caracter (comodin)
\ caracter de escape, se utiliza en la busqueda de otros cuantificadores como cadenas literales
[] rango de caracteres validos: [0-6] significa "entre e incluyendo 0-6"
{} Cuantos caracteres estan permitidos dentro de la regla del patron previamente definido

Con estos cuantificadores, podemos ser mucho mas especificos en lo que estamos buscando y donde lo estamos buscando para ello. He aqui algunos ejemplos:


echo preg_match('/^verdes/', $string) ; // devuelve 0
echo preg_match('/^las ideas/', $string) ; // devuelve 1
echo preg_match('/^duermen/', $string) ; // devuelve 0
echo preg_match('/furiosamente$/', $string) ; // devuelve 1
echo preg_match('/verd.s/', $string) ; // devuelve 1


Si $matches es proporcionado, entonces se llena con resultados de la busqueda. $matches[0] contendra el texto que coincidio con el patron completo, $matches[1] tendra el texto que coincidio con el primer sub patron con parentesis capturado, y asi sucesivamente. Vamos a trabajar a traves de un ejemplo completo a continuacion.

Obteniendo la codificacion de caracteres de una pagina web

Cada pagina web tiene una codificacion especial para seleccionar que rangos de caracteres se muestran por el navegador. Hay varias maneras de especificar que codificacion de caracteres seran usadas en el documento. En primer lugar, el servidor web puede incluir la codificacion de caracteres o 'charset' en el protocolo de transferencia de hipertexto (HTTP) en la cabecera Content-Type, que normalmente tiene el siguiente aspecto:

Content-Type: text/html; charset=ISO-8859-1

Para HTML, es posible incluir esta informacion en el interior del elemento head, cerca de la parte superior del documento:

<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

En el siguiente ejemplo breve obtendra la codificacion de caracteres para una pagina web en particular. Vamos a usar las  funciones PHP regex para buscar el contenido requerido. Aqui estamos mirando para el atributo 'charset' que incluye la informacion de codificacion.

<?php
/* Obtenemos el contenido del sitio */
$html = file_get_contents('http://www.microsoft.com/');
// Buscamos el charset en el atributo meta
preg_match('/charset\=.*?(\'|\"|\s)/i', $html, $matches);
//Reemplazamos lo que no necesitamos a vacio
$matches = preg_replace('/(charset|\=|\'|\"|\s)/', '', $matches[0]);
echo strtoupper($matches);
?>


La parte importante del codigo es la expresion regular para buscar el patron 'charset'.

'/charset\=.*?(\'|\"|\s)/i' 

La funcion preg_match se utiliza para encontrar la primera cadena que coincide para el patron anterior. Una vez que se encuentra, la funcion preg_replace solo mantiene el codigo del charset real y devuelve eso. Usted puede haber notado el caracter 'i' en la expresion regular despues del delimitador de cierre. Esto indica que la coincidencia debe ser case insensitive (insensible a mayusculas y minusculas). La funcion preg_match() solo coincide con la primera ocurrencia (coincidencia) encontrada y luego se detiene. La funcion preg_match_all() coincide sin embargo en varias ocasiones hasta que la ultima ocurrencia termina, hasta que no haya mas ocurrencias que encontrar. preg_match_all() funciona exactamente igual que preg_match(); sin embargo, la matriz resultante es multidimensional. Cada entrada de esta matriz es un arreglo de ocurrencias, ya que habria sido devuelta por preg_match().

Agarrando imagenes de una pagina web

Tomemos otro ejemplo. Una de las tareas comunes en web scraping es agarrar las imagenes de una pagina web. Esto se puede lograr facilmente usando el poder de la expresion regular. El siguiente codigo le permite obtener una lista de todas las imagenes de una pagina web junto con sus atributos - como src, height, width, alt etc. Para este ejemplo vamos a separar la parte de la expresion regular desde el script principal como es un poco largo y seria mejor si lo hacemos como una funcion. Llamamos a esta funcion 'parseTag'. La funcion 'parseTag' tambien extrae los atributos de la etiqueta de la imagen. La funcion completa es la siguiente.

function parseTag($tag, $content)
{
$regex = "/<{$tag}\s+([^>]+)>/i";
preg_match_all($regex, $content, $matches);
$raw = array();
//Nosotros tambien queremos los atributos de la etiqueta
foreach($matches[1] as $str)
{
$regex = '/([a-z]([a-z0-9]*)?)=("|\')(.*?)("|\')/is';
preg_match_all($regex, $str, $pairs);
if(count($pairs[1]) > 0) {
$raw[] = array_combine($pairs[1],$pairs[4]);
}
}
return $raw;
}


El trabajo pesado del codigo anterior es la funcion preg_match_all(), que toma todas las etiquetas epecificadas en el parametro de una pagina web. A continuacion se muestra el codigo completo para agarrar los enlaces de la imagen de nuestra template usando la funcion 'parseTag'. Hemos utilizado cURL abajo, pero tambien podriamos utilizar file_get_html(). Ambas versiones se muestran aqui.

<?php
/* version cURL */
$url = 'http://localhost/scrape/template/index.html';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_TIMEOUT, 6);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$html = curl_exec($ch);
/* Esto grabara todos los datos de la etiqueta <img> */
$links = parseTag('img', $html);
print_r($links);
?>


La version file_get_html() se muestra a continuacion. Como se puede ver, es mas concisa que la version cURL. 
 
<?php
/*
version file_get_html  */
$url = 'http://localhost/scrape/template/index.html';
$html = file_get_html($url);
/* Esto grabara todos los datos de la etiqueta <img> */
$links = parseTag('img', $html);
print_r($links);
?>


Esto devolvera la siguiente matriz. Tenga en cuenta los atributos adicionales devueltos incluyendo los nombres de las clases de las imagenes. Esto puede ser util cuando solo se necesita encontrar imagenes con un nombre de clase determinada o etiqueta alt.

Array
(
[0] => Array
(
[src] => images/flower1.jpg
[class] => imageb
[alt] => flower 1
)
[1] => Array
(
[src] => images/flower2.jpg
[class] => imageb
[alt] => flower 2
)
[2] => Array
(
[src] => images/flower3.jpg
[class] => imageb
[alt] => flower 3
)
)


Tambien puede imprimir el atributo src de cada imagen dentro de un bucle for.

foreach($links as $link) {
echo $link['src'] . '<br>';
}


Como se puede ver las expresiones regulares junto a simplehtmldom pueden ayudar a raspar cualquier contenido web que usted desee. Por supuesto, las expresiones regulares son mas complejas que el uso de una libreria como simplehtmldom, pero ofrecen mas flexibilidad y potencia si se utiliza correctamente. Pero el principal inconveniente es la curva de aprendizaje que exige. Si se diseña incorrectamente las expresiones regulares puede ser una fuente de errores sutiles. Por estas razones, prefiero usar simplehtmldom siempre que sea posible y solo recurrir a expresiones regulares en casos raros.

1 comentario: