domingo, 18 de enero de 2015

[Parte 3] Web Scraping

Herramientas para web scraping


Al igual que con cualquier otra tarea, comenzando con un buen conjunto de herramientas para comprender y utilizarlas de manera eficiente. Web scraping no es diferente. Web scraping se puede hacer ya sea mediante programacion con lenguajes de scripting como PHP, Ruby, o con ayuda de herramientas como wget o curl, aunque mas tarde no proporcionan la flexibilidad de un lenguaje que son herramientas utiles que vendran a mano. Cada uno puede ser utilizado de manera independiente o con la combinacion de realizar ciertas tareas de web scraping. Nuestro principal objetivo es utilizar PHP para recuperar paginas web y raspar la pagina para contenidos que nos interesan.

Trabajar con PHP para raspar los datos de las paginas webs no es una tarea facil y requiere de un buen conocimiento de las expresiones regulares. Sin embargo, existen algunas excelentes librerias, que seran muy faciles para analizar los datos de una pagina web sin ningun conocimiento de expresiones regulares. Tener un conocimiento practico de ello es parte esencial de un programador y puede ayudar enormemente con su trabajo de raspado (scraping).

En este apartado vamos a trabajar exclusivamente con la maravillosa herramienta SimpleHTMLDOM. Esta es una libreria muy pequeña y versatil, y es facil de integrar dentro de su aplicacion. Es un analizador HTML DOM escrito en PHP5+ que permite manipular HTML de manera muy facil. Es compatible con HTML invalido y permite encontrar las etiquetas en una pagina HTML con selectores como jQuery. Se puede descargar desde el siguiente enlace: http://simplehtmldom.sourceforge.net/.

La mejor manera para obtener una sensacion de utilizar la libreria es conseguir un par de ejemplos de como se ejecuta. Una vez que estamos a traves de estos ejemplos, vamos a ver mas profundamente la libreria. Aunque hay muchas librerias PHP por ahi que pueden ayudarte en su web scraping, centrandose unicamente en una sola libreria le ayudara conseguir el dominio de la libreria, depurando y escribiendo mas rapido sus scrapers. Ademas una vez que se llega a conocer una libreria se puede pasar facilmente a otras librerias. Simple HTML DOM ha existido desde hace bastante tiempo y es estable y altamente utilizado. Antes de empezar con simplehtmldom, miraremos otra herramienta muy importante cURL.

PHP cURL

Si quieres ver cURL en profundidad te invito a que visites las siguientes partes:

http://arthusu.blogspot.mx/2014/04/parte-1-curl-en-php.html
http://arthusu.blogspot.mx/2014/04/parte-2-curl-en-php.html
http://arthusu.blogspot.mx/2014/04/parte-3-curl-en-php.html
http://arthusu.blogspot.mx/2014/04/parte-4-curl-en-php.html
http://arthusu.blogspot.mx/2014/05/parte-5-curl-en-php.html

Mientras PHP el mismo es capaz de descargar archivos remotos y paginas webs, la mayoria de las aplicaciones de raspado web requiren una funcionalidad adicional para manejar temas avanzados como envio de formularios, autenticacion, redireccion y asi sucesivamente. Estas funciones son dificiles de facilitar con funciones incorporadas de solo PHP. Afortunadamente para nosotros cada instalacion de PHP incluye una libreria llamada PHP/CURL, que se encargar automaticamente de estas caracteristicas avanzadas. La mayoria de los ejemplos que veras hacen uso de cURL para descargar archivos y paginas web. 

A diferencia de las funciones de red incorporadas en PHP, cURL soporta multiples protocolos de transferencia - FTP, FTPS, HTTP, HTTPS, Gopher, Telnet y LDAP. De estos protocolos el mas importante para nuestro proposito es probablemente HTTP y HTTPS. Permite a nuestros web scrapers la descarga de paginas webs encriptadas que utilizan el protocolo SSL (Secure Sockets Layer).

Como se indico anteriormente, cURL permite la transferencia de datos atraves de una amplia variedad de protocolos. Es ampliamente utilizado como una forma de enviar contenido a traves de sitios web, incluyendo cosas como API. cURL no se restringe en lo que puede hacer, a partir de una peticion basica HTTP, carga de archivos en FTP o interaccion con un sitio seguro HTTPS.

Antes de que podamos hacer algo con una solicitud cURL, tenemos que crear antes una instancia de un recurso cURL llamando a la funcion curl_init(). Esta funcion toma un parametro que es la direccion URL a la que desea enviar la solicitud y devuelve un recurso cURL.

$ch = curl_init('http://ejemplo.com');

Tambien podemos inicializar cURL de una manera ligeramente diferente:

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://ejemplo.com');


Tomemos un ejemplo sencillo:

<?php
$ch = curl_init('http://ejemplo.com');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$res = curl_exec($ch);
curl_close($ch);
?>


Los detalles de cada linea se indican a continuacion:

* curl_init se llama y se pasa 'http://ejemplo.com' como una url para la solicitud
* curl_setopt es llamada a establecer la configuracion de ajuste representado por CURLOPT_RETURNTRANSFER estableciendo el valor en verdad (true). Esta configuracion hará que curl_exec devuelva el cuerpo de la respuesta HTTP en una cadena saliendo directamente al navegador o consola, siendo este ultimo el comportamiento predeterminado. 
* curl_exec es llamado para ejecutar la solicitud y devolver el cuerpo de la respuesta que se almacena en la variable $res
* curl_close es llamado explicitamente para cerrar el identificador de sesion curl. Es importante que compruebe el codigo de retorno HTTP despues de una operacion cURL. 

Puede utilizar la funcion 'curl_getinfo' para obtener la respuesta HTTP.

$estado_http = curl_getinfo($ch);

La impresion de la variable $estado_http sera la siguiente informacion:

Array
(
[url] => http://www.ejemplo.com
[content_type] => text/html; charset=UTF-8
[http_code] => 200
[header_size] => 157
[request_size] => 58
[filetime] => -1
[ssl_verify_result] => 0
[redirect_count] => 0
[total_time] => 1
[namelookup_time] => 0.109
[connect_time] => 0.406
[pretransfer_time] => 0.406
[size_upload] => 0
[size_download] => 0
[speed_download] => 0
[speed_upload] => 0
[download_content_length] => 20714
[upload_content_length] => -1
[starttransfer_time] => 1
[redirect_time] => 0
)


Apartir de la matriz anterior solo estamos interesados en el codigo de estado, el cual podemos comprobar con lo siguiente:


$http_status = curl_getinfo($ch);
if($http_status['http_code'] != 200) {
echo "echo error.";
// Hacer algo aqui
}  


Tambien puede obtener el codigo de estado HTTP directamente atraves de la opcion 'CURLINFO_HTTP_CODE':

$estado_http = curl_getinfo($s, CURLINFO_HTTP_CODE);

Simple HTML DOM: Una vision rapida

Comencemos con un ejemplo del mundo real para tener una idea de la libreria. El siguiente es un programa sencillo que busca en google la palabra clave 'flor' e imprime todos los enlaces de la pagina:

<?php
/* Incluimos la libreria simplehtmldom */
require_once('simplehtmldom/simple_html_dom.php');
/* Obtenemos el contenido de la pagina por el buscador de Google */
$html = file_get_html('http://www.google.com/search?q=flor');
/* Buscamos todos los enlaces '<a>' de la pagina */
$links = $html->find('a');
/* Recorremos todos los links y los imprimimos */
foreach($links as $elemento)
{
echo $elemento->href . '<br>';
}
?>


El codigo se explica por si mismo. Inicialmente incluimos la libreria 'simplehtmldom' en su programa. Asegurese de la que la ruta de la libreria esta correcta. 

require_once('simplehtmldom/simple_html_dom.php');

A continuacion, utilizamos la funcion de la libreria file_get_html() para obtener el contenido en raw de la URL dada. La variable $html ahora contendra la estructura DOM completa de la pagina web recuperada. 

$html = file_get_html('http://www.google.com/search?q=flor');

Tenga en cuenta que la funcion file_get_html() utiliza file_get_contents() internamente de PHP. Asi que si file_get_contents() esta desactivada por su proveedor por razones de seguridad tendra que habilitarlo en su archivo de configuracion php.ini o en su lugar usar curl para obtener el contenido de la pagina inicial. 

Una vez que se ejecuta la linea anterior, la variable $html contiene el objeto simplehtmldom que contiene el contenido HTML de la url dada. Una vez que tenemos nuestra pagina el DOM, estamos dispuestos a buscar con el metodo 'find' de la libreria. En este ejemplo estamos buscando el elemento de enlace <a>.

$links = $html->find('a');

Esto devolvera una matriz de elementos del objeto <a>, que luego iteramos para imprimir el atributo href, el ejemplo anterior utilizando curl es el siguiente:

<?php
/* Incluimos la libreria simplehtmldom */
require_once('simplehtmldom/simple_html_dom.php');
/* Obtenemos el contenido de la pagina desde el buscador Google */
$ch = curl_init('http://www.google.com/search?q=flor');
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$html_data = curl_exec($ch);
curl_close($ch);
/* Creamos un objeto DOM desde la pagina anteriormente dada. */
$html = str_get_html($html_data);
/* Buscamos todos los enlaces de la pagina '<a>'. */
$links = $html->find('a');
/* Los recorremos e imprimimos */
foreach($links as $elemento) {
echo $elemento->href . '<br>';
}
?>

 
En el ejemplo anterior hemos utilizado la siguiente linea para imprimir el atributo href:

echo $elemento->href;

Esto es el flujo de cualquier web scraping. Por supuesto, para proyectos complejos que habra demasiadas variaciones que habra que explorar, pero la logica basica y el flujo seguira siendo el mismo. Cada vez que buscas todos los elementos DOM, varios atributos se devuelven con la busqueda. En el ejemplo anterior hemos utilizado href, pero podria haber otros atributos que estan disponibles mediante el uso de los siguientes datos:

print_r(array_keys($elemento->attr));

Esto deberia de devolver algo como lo siguiente:

Array
(
[0] => href
[1] => rel
[2] => title
)


Asi que hay un atributo de title el cual puede ser impreso en lugar del atributo href.

echo $elemento->title;

Cuando se especifica un atributo para imprimir, como el titulo en el ejemplo anterior, simplehtmldom busca primero en la lista de atributos para ver si hay algun atributo con ese nombre disponible, si lo encuentra entonces devuelve eso o lanza error. Muchas veces no son necesarios los atributos, pero el texto real dentro del elemento DOM, por ejemplo, el texto dentro de la etiqueta h3, para ello podemos utilizar el metodo 'plaintext' o 'innertext' el cual devuelve el contenido html en formato raw dentro del elemento especificado, mientras 'plaintext' devuelve el texto en plano sin formato html. Hay otro metodo 'outertext' que devuelve el texto exterior del nodo DOM junto a las etiquetas. El siguiente ejemplo muestra como los diferentes metodos son devueltos desde una cadena:

$html = str_get_html("<div>Hola <b>Mundo!</b></div>");
$ret = $html->find("div", 0);
echo $ret->tag; // "div"
echo $ret->outertext; // "<div>Hola <b>Mundo!</b></div>"
echo $ret->innertext; // "Hola <b>Mundo!</b>"
echo $ret->plaintext; // "Hola Mundo!"


Supongamos que queremos listar todos los titulos de la busqueda de Google, en lugar de los enlaces podemos utilizar el codigo dado a continuacion, los titulos estan dentro de la etiqueta h3, por lo que tendran que buscar la misma, aviso lo facil es cambiar el elemento de busqueda el elemento de busqueda es el metodo 'find':

/* Buscamos todos los titulos '<h3>' en los elementos de la pagina */
$titulos = $html->find('h3');
/* recorremos atraves de todos los titulos y los imprimimos */
foreach($titulos as $titulo) {
echo $titulo->plaintext . '<br>';
}


Hemos utilizado el metodo 'plaintext' aqui, en lugar del nombre de los atributos.

Guardando en caché paginas descargadas

Durante su inicio en proyectos de web scraping cuando se esta en aprendizaje, le ayudara enormemente si guarda una pagina en local a la cual le esta realizando web scraping. Esto ahorrara su ancho de banda y tambien de la pagina se que este raspando. Puede suceder que si se golpea el servidor remota de forma frecuente puede prohibir su IP. La mayoria de los datos de la pagina web no cambian con frecuencia, podemos ahorrarnos esto con las noticias o cosas relacionadas. Asi que una vez que haya guardado la pagina localmente y que se sienta comodo para probar el codigo de scraping en la copia local en lugar de ir de nuevo al servidor. Asi que si usted esta haciendo web scraping a la pagina principal de yahoo, se puede guardar un archivo local inicialmente y hacer uso de el mas adelante para rasparlo. Esto ayudara a mantener su trafico bajo y aumentar su velocidad es por que esta accediendo a una copia local, en lugar de la pagina web. A continuacion se muestra el codigo para guardar una copia local de la pagina:

/* Obtenemos el contenido de la pagina principal de yahoo */
$homepage = file_get_contents('http://www.yahoo.com/');
/* Lo guardamos en un archivo local */
file_put_contents("cache/index.html", $homepage);


Si esta utilizando curl puede utilizar lo siguiente:


/* obtenemos la pagina principal de yahoo */
$ch = curl_init('http://yahoo.com');
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$html_data = curl_exec($ch);
curl_close($ch);
file_put_contents("cache/index.html", $html_data);


Ahora que usted tiene la copia local de la pagina principal, se puede utilizar en el codigo de raspado:


/* Incluimos la libreria simplehtmldom */
require_once('simplehtmldom/simple_html_dom.php');
/* Creamos un objeto DOM desde el archivo local */
$html = file_get_html('cache/index.html');
/* Buscamos todos los enlaces <a> de la pagina */
$links = $html->find('a');
/* Recorremos todos los enlaces y los imprimimos */
foreach($links as $element) {
echo $element->href . '<br>';
}


Depuracion cURL

Rara vez un codigo nuevo corre perfectamente la primera vez. cURL no es la excepcion. Muchas veces puede no funcionar correctamente o simplemente devolver un error. La funcion curl_getinfo permite ver peticion enviadas por cURL. Esto puede ser muy util para depurar las solicitudes, a continuacion se presenta un ejemplo de esta funcion en accion:

$ch = curl_init('https://www.google.com/search?q=flor&start=0');
curl_setopt($ch, CURLOPT_TIMEOUT, 6);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
$html = curl_exec($ch);
$request = curl_getinfo($ch, CURLINFO_HEADER_OUT);
curl_close($ch);


La variable $request ahora mantiene el contenido de la peticion enviada por cURL. Puede ver si esto es correcto y modificar el codigo en consecuencia.


Nos vemos en la proxima parte.
 

No hay comentarios:

Publicar un comentario en la entrada