domingo, 25 de enero de 2015

[Parte 4] Web Scraping

Explorando Simple HTML DOM

Simple HTML DOM en Detalle

Ahora que hemos visto un par de ejemplos, vamos a mirar en detalle esta maravillosa herramienta. Para todos los ejemplos a continuacion vamos a utilizar la plantilla siguiente: https://docs.google.com/file/d/0B_pyVfgTOpujbkFISXA5MGFUUFU/edit

La razon por la cual no utilizamos sitios webs reales es que en esos sitios el html cambia con frecuencia, lo cual podria suceder que los ejemplos queden sin ser accedidos.

Construyendo un DOM inicial

Como vamos a trabajar con la plantilla proporcionada, primero debe instalarlo en su directorio local. Para los ejemplos asumiremos que tiene una URL local la cual se muestra a continuacion, aunque la suya puede variar, por lo que necesita ajustar los ejemplos en consecuencia.

http://localhost/scrape/template/index.html

Trabajar con simplehtmldom requiere que primero creemos una estructura DOM con el HTML de la pagina que nos interesa. Todas las funciones proporcionadas por simplehtmldom trabajan con este objeto DOM y no con el texto plano de la pagina. Asi que el objeto DOM es lo que necesitamos para empezar. El objeto DOM puede ser creado de 3 maneras:

/* Creamos un objeto DOM a partir de una cadena */
$html = str_get_html('<html><body>¡Hola!</body></html>');
/* Creamos un objeto DOM a partir de una URL */
$html = file_get_html('http://www.google.com/');
/* Creamos un objeto DOM a partir de un archivo HTML */
$html = file_get_html('index.html');


Todo lo anterior devuelve un objeto DOM que es almacenado en la variable $html. str_get_html() y file_get_html() son funciones de simplehtmldom. Como se dijo anteriormente, la funcion file_get_html() utiliza la funcion interna file_get_contents() de PHP. Asi que si esa funcion esta desactivada de su sistema por cualquier razon, tendra que utilizar cURL para obtener el contenido de la pagina inicial. Una vez obtenido el contenido usando cURL, puede utilizar la funcion str_get_html() para crear un objeto DOM. Si esta usted mas inclinado al estilo de programacion orientado a objetos puede hacer lo siguiente en lugar de crear el DOM.


/* Creamos un objeto DOM */
$html = new simple_html_dom();
/* Leemos el HTML desde una cadena */
$html->load('<html><body>¡Hola!</body></html>');
/* Leemos el HTML desde una URL */
$html->load_file('http://www.google.com/');
// Leemos el HTML desde un archivo
$html->load_file('index.html');


Aunque no se menciona anteriormente, str_get_html y file_get_html pueden tomar parametros adicionales junto a la cadena HTML. 

str_get_html($str,
$lowercase=true, //Forza a minusculas para el nombre de etiquetas
$forceTagsClosed=true,
$target_charset = DEFAULT_TARGET_CHARSET,
$stripRN=true, // Strip NewLine characters
$defaultBRText=DEFAULT_BR_TEXT)


Todos los parametros tienen un valor por defecto, lo que va hacer o la mayoria de las aplicaciones. Sin embargo es necesario tener cuidado. El quinto parametro - $defaultBRText indica que todos los caracteres que tengan un salto de linea seran cortados como una cadena. Esto aveces puede causar problemas cuando se utiliza la cadena en otro lugar y se espera que la cadena tenga un salto de linea. Que por defecto esta establecido a “\r\n” (Windows). Si esta trabajando con saltos de linea de estilo *NIX, es necesario ponerlo en "\n".

El tercer parametro - $forceTagsClosed, tambien es importante. Forzar a las etiquetas se cierran significa que no confiamos en el HTML, esto puede ser util en casos de mal marcado HTML. Pero en validaciones de HTML es mejor establecer este valor como false (falso). La siguiente es la lista de parametros para la funcion file_get_html.


file_get_html($url,
$use_include_path = false,
$context=null,
$offset = -1,
$maxLen=-1,
$lowercase = true,
$forceTagsClosed=true,
$target_charset = DEFAULT_TARGET_CHARSET,
$stripRN=true, $defaultBRText=DEFAULT_BR_TEXT)



Los ultimos parametros son los mismos que para la funcion str_get_html, mientras que los primeros 5 parametros son los mismos que para la funcion file_get_contents de PHP, como file_get_html llama a esta funcion internamente.

Agarrando el contenido del texto de una pagina

/* Incluimos la libreria simplehtmldom */
require_once('simple_html_dom.php');
$template = 'http://localhost/scrape/template/index.html';
$html = file_get_html($template);
echo $html->plaintext;


Esto imprimira todo el contenido de la pagina sin las etiquetas html, es decir, el texto plano de la pagina. Una forma de acceso directo seria asi.

/* Volcar el contenido (sin etiquetas) desde HTML*/
echo file_get_html($template)->plaintext;


Si usted necesita el contenido de texto sin formato de una pagina web externa en su lugar puede utilizar lo siguiente.

/* Volcar el contenido (sin etiquetas) desde HTML*/ 
echo file_get_html('http://www.yahoo.com')->plaintext;

Esto puede ser util si usted necesita procesar algun texto en alguna pagina web completa, al igual que con que frecuencia se utilizan las palabras para crear una nube de etiquetas, encontrar palabras claves importantes, etc.

Encontrar elementos en una pagina

Todo el proceso de descarga de una pagina web es que podemos recuperar algo de contenido que nos interesa. El metodo find de simplehtmldom es el metodo principal para este fin. La funcion toma un selector como parametro y devuelve una matriz de elemento de objetos encontrados. Algunos ejemplos se muestran a continuacion.


/* busca la etiqueta 'a' en el html */
$ret = $html->find('a');


/* busca todas las etiquetas  <div>  que tengan el atributo id establecido */
$ret = $html->find('div[id]');



/* busca todas las etiquetas <div> que tengan el id=comment */
$ret = $html->find('div[id=comment]');


/* busca todos los elementos que tienen el id=comment */
$ret = $html->find('#comment');


/* busca todos los elementos que tienen la class=comment */
$ret = $html->find('.comment');


/* busca todos los elementos que tienen el atributo id establecido */
$ret = $html->find('*[id]');


/* busca a todos los elementos con anclas e imagenes */
$ret = $html->find('a, img');


Digamos por ejemplo que usted desea conseguir todos los titulos de los enlaces en la barra lateral de nuestra plantilla de ejemplo.



Usando firebug nos damos cuenta que los enlaces estan dentro de las etiquetas a u y la clase sidemenu. Asi que podemos pedir que el metodo find busque todos los enlaces que se encuentran debajo de la etiqueta ul con un nombre de clase dado.

$links = $html->find('ul[class=sidemenu] a');
/* Recorremos todos los links y los imprimimos */
foreach($links as $link)
{
echo $link->plaintext . '<br>';
}


Podemos comprobar aun mas que esten disponibles otros atributos para su uso.

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

Esto devolvera lo siguiente en nuestro caso.

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


Asi que podemos imprimir los titulos en lugar de los enlaces.

$links = $html->find('ul[class=sidemenu] a');
/* Recorremos todos los enlaces e imprimimos el 'title' */
foreach($links as $link)
{
echo $link->title . '<br>';
}


Simplehtmldom tambien tiene algunos metodos para trabajar con atributos. Si usted necesita ver todos los atributos que tiene un elemento puede utilizar el metodo getAllAttributes(). Asi que para el ejemplo anterior lo siguiente imprimira todos los atributos del elemento $link.

print_r($link->getAllAttributes());

Imprimira:

Array
(
[title] => side menu 5
[href] => #
)


Podemos comprobar si un elemento tiene un atributo en particular establecido y lo devuelve.

if($link->hasAttribute('title'))
{
echo $link->getAttribute('title');
}


Otro ejemplo - Supongamos que queremos encontrar los enlaces del pie de pagina en nuestra pagina del template. Podemos ver usando Firebug que estan bajo un div con el id de nombre footerleft.


Una vez que sabemos el nombre del id podemos buscarlo con find.

$links = $html->find('div[id=footerleft] a');

Tambien podriamos haber utilizado la siguiente linea al llegar a los links, pero el ul es redundante en este caso.

$links = $html->find('div[id=footerleft] ul a');

No hay una manera unica de llegar al elemento que te interesa. Este es un truco que usted va aprender con el tiempo una vez que comprenda la libreria y tener alguna experiencia con el raspado web en su cinturon. Los anteriores son ejemplos descendientes de selectores, algunos mas los mostramos a continuacion.

/* Buscamos todos los <li> en <ul> */
$ret = $html->find('ul li');


/* Buscamos etiquetas <div> anidadas */
$ret = $html->find('div div div');


/* Buscamos todos los <td> en una <table> con class=salary */
$ret = $html->find('table.salary td');


/* Buscamos todos las etiquetas <td> con el atributo align=center
en una etiqueta <table>  */
$ret = $html->find('table td[align=center]');


Iterar sobre los elementos anidados

Digamos que usted desea iterar sobre todos los elementos div en nuestra pagina del template que tienen la clase ct e imprimir el contenido. El codigo sera de la siguiente manera.

$findDivs = $html->find('div[class=ct]');
foreach($findDivs as $findDiv)
{
echo $findDiv->plaintext . '<br />';
}


Por ejemplo, si desea buscar todas las etiquetas <p> dentro de las etiquetas <div>, lo que podemos escribir es lo siguiente.

foreach($findDivs as $findDiv)
{
foreach($findDiv->find('p') as $p)
{
echo $p->plaintext . '<br />';
}
}


Por supuesto, esto es equivalente a lo siguiente. Que el estilo a utilizar dependera de su problema en particular. Lo anterior es mas flexible si quieres buscar varias etiquetas dentro de la etiqueta div y desea procesar mas etiquetas.

$findDivs = $html->find('div p');
foreach($findDivs as $findDiv)
{
echo $findDiv->plaintext . '<br />';
}


Scraping tablas HTML

Las tablas HTML son uno de los elementos mas importantes de una pagina y puede requerir mucho esfuerzo para raspar utilizando expresiones regulares. Pero simplehtmldom hace que sea mas facil para iterar sobre todas las filas y columnas de las tablas. Nuestro template de muestra tiene 3 filas que utiliza el ejemplo de abajo.

<table id="mytable">
<thead>
<tr>
<th width="150">Column 1</th>
<th width="150">Column 2</th>
<th width="150">Column 3</th>
<th width="150">Column 4</th>
</tr>
</thead>
<tbody>
<tr>
<td>C-1 R-1 Content</td>
<td>C-2 R-1 Content</td>
<td>C-3 R-1 Content</td>
<td>C-4 R-1 Content</td>
</tr>
<tr>


La mejor manera de obtener todo el contenido de la tabla es buscar e iterar sobre cada <tr>. A continuacion se muestra el codigo completo.

$table = $html->find('table[id=mytable]');
foreach ($table as $t) {
foreach ($t->find('tr') as $tr) {
foreach ($tr->find('td') as $td) {
echo $td->innertext . " | " ;
}
echo "<br>";
}
}


Esta salida luce como lo siguiente:
 

Col-1 Row-1 Content | Col-2 Row-1 Content | Col-3 Row-1 Content | Col-4 Row-1 Content | 
Col-1 Row-2 Content | Col-2 Row-2 Content | Col-3 Row-2 Content | Col-4 Row-2 Content | 
Col-1 Row-3 Content | Col-2 Row-3 Content | Col-3 Row-3 Content | Col-4 Row-3 Content | 

Tenga en cuenta que en el ejemplo anterior, si solo hay una tabla con el id 'mytable' usted puede tambien escribir el codigo de la siguiente manera. Observe como estamos indexando la variable $table.

$table = $html->find('table[id=mytable]');
foreach ($table[0]->find('tr') as $tr) {
foreach ($tr->find('td') as $td) {
echo $td->innertext . " | ";
}
echo "<br>";
}


Si habia dos tablas y queriams el contenido de la segunda tabla el indice de la tabla sera diferente.

foreach ($table[1]->find('tr') as $tr)

A veces puede haber la necesidad de almacenar los contenidos de la tabla en una matriz de dos dimensiones para su posterior procesamiento en lugar de hacer echo a la pantalla. Tal vez usted quiere guardarlo en una base de datos o exportarlo en un archivo CSV. Cualquiera que sea el caso, podemos agregar facilmente algunas lineas adicionales para guardar el contenido de la tabla en una matriz.

$table_array = array();
$row = 0;
$col = 0;
$table = $html->find('table[id=mytable]');
foreach ($table as $t) {
foreach ($t->find('tr') as $tr) {
foreach ($tr->find('td') as $td) {
$table_array[$row][$col] = $td->plaintext;
$col++;
}
$row++;
}
}
print_r($table_array); // Prueba del array


Fijate como hemos usado un bucle for para iterar sobre las columnas de la tabla. Tambien puede acceder a columnas o filas individuales utilizando un indice en lugar de utilizar un bucle for.

$td = $tr->find('td') ;
echo $td[1]; //mostrando la segunda columna de la tabla


DOM Padres e Hijos

Una estructura de la pagina HTML siempre es jerarquica. Todos los elementos DOM tienen padres e hijos. Una pequeña parte de nuestra plantilla es la siguiente.

<!DOCTYPE html>
<html>
<head>
<title>Web Scraping Template</title>
</head>
<body>
<div id="container" class="clearfix">
<div id="menucont">
<ul>
<li><a title="" href="#" class="active">Home</a></li>
<li><a title="" href="#">About Us</a></li>
<li><a title="" href="#">Blog</a></li>
<li><a title="" href="#">Contact Us</a></li>
</ul>
</div>


Usted puede notar que la etiqueta div con el id "container" es hijo de la etiqueta body. Si utilizamos el siguiente codigo en nuestro template este devolvera el padre del div container como la etiqueta body. De esta manera se puede recorrer el arbol DOM usando el metodo parent.

$findDivs = $html->find('div[id=container]');
foreach($findDivs as $findDiv)
{
echo $findDiv->parent()->tag; // Imprime el 'body'
}


Digamos que queremos encontrar todas las etiquetas p, pero solo aquellos que son hijos inmediatos de un elemento div con una clase llamada ct. Podriamos utilizar el metodo parent aqui. El fragmento de codigo HTML se da a continuacion,

<div class="ct">
<p>Div 1. pellentesque</p>
</div>


El codigo para analizar lo anterior se muestra a continuacion.

$findDivs = $html->find('p');
foreach($findDivs as $findDiv)
{
if ($findDiv->parent()->class == 'ct' &&
$findDiv->parent()->tag == 'div')
{
echo $findDiv->plaintext;
}
}


Por supuesto que podriamos haber escrito de manera diferente especificando una consulta mas precisa con el metodo find.

$findDivs = $html->find('div[class=ct] p');
foreach($findDivs as $findDiv) {
echo $findDiv->plaintext;
}


Entonces ¿por que necesitamos un metodo parent? Muchas veces la estructura HTML cambia con frecuencia y tenemos que ser mas flexibles en nuestros esfuerzos de analisis. El metodo parent permite la flexibilidad al escribir el codigo, por lo que cualquier estructura de la pagina los cambios se pueden acomodar facilmente. Otro metodo para completar el metodo parent es el metodo children(n). Esto devuelve el objeto secundario Nth si el indice esta establecido, de lo contrario devuelve una matriz de children. Asi por ejemplo, lo siguiente imprimira los nombres de las etiquetas de los hijos de la div con clase ct. Tenga en cuenta una vez mas que los ejemplos que estan aqui son tomados con el template proporcionado. 


$findDivs = $html->find('div[class=ct]');
foreach($findDivs as $findDiv) {
foreach($findDiv->children as $child) {
echo $child->tag;
}
}
// Imprime 'p'


Otros metodos para su uso junto con children se dan a continuacion.

$e->first_child () //Devuelve el primer hijo del elemento, o null.
$e->last_child () //Devuelve el ultimo hijo del elemento, o null.
$e->next_sibling () //Devuelve el siguiente hermano del elemento o null.
$e->prev_sibling () //Devuelve el hermano anterior del elemento o null.



Metodos de encadenamiento

Los metodos de encadenamiento son una manera mas facil de perforar a profundidad la jerarquia de los elementos DOM. Lo siguiente por ejemplo, nos permitira acceder a los elementos children anidados de la etiqueta div a p.

$findDivs = $html->find('div[id=container]');
foreach($findDivs as $findDiv) {
echo $findDiv->children(1)->children(0);
}


Esto imprimira lo siguiente.

Web Scraping Template

Descarga de imagenes

La descarga de imagenes es una de las tareas mas comunes de web scraping que un desarrollador se puede encontrar. Con la ayuda de simplehtmldom y curl podemos lograr esto facilmente. Tomemos nuestro template de muestra de nuevo. Esto tiene tres imagenes que deseamos descargar a nuestro directorio local. Usando firebug podemos ver que las imagenes estan en la etiqueta p dentro de un div con la clase images. El pantallazo de firebug se muestra a continuacion.

 
Desde firebug el diseño del DOM nosotros podemos conseguir las urls de la imagen con el siguiente codigo.

$images = $html->find('div[class=images] img');
foreach($images as $image)
{
echo $image->src;
}


Tenga en cuenta que el src de la imagen tiene una ruta relativa, que ahora tenemos que convertir en absoluta para descargar las imagenes. Una vez que nosotros somos completamente capaces de obtener el src de las imagenes ahora nosotros podemos descagarlas usando la funcion de PHP file_get_contents().

$images = $html->find('div[class=images] img');
/* Ajustamos esto para la ruta completa al template */
$url = 'http://localhost/scrape/template/';
foreach($images as $image)
{
/* Obtenemos el link de la imagen */
$image_src = $image->src;
/* El atributo src tambien contiene el nombre del directorio
'images/' , necesitamos deshacernos de el para obtener el nombre de la imagen en texto plano */
$file_name = str_replace('images/', '', $image_src);
/* Nosotros descargamos los datos de la imagen y lo guardamos como archivo */
$file = file_get_contents($url . $image_src);
$fp = fopen($file_name, 'w');
fwrite($fp,$file);
fclose($fp);
}


En el escenario anterior se realizaron busquedas de las imagenes y a la vez se descargaron las imagenes en nuestro directorio local. Sin embargo, muchas veces es mejor guardar simplemente las direcciones URL de imagen en un archivo y luego descargar las imagenes con otro script.

$fp = fopen('image_urls.txt', 'w');
foreach($images as $image)
{
/* Obtenemos los links de la imagen */
$image_src = $image->src;
fwrite($fp,$url . $image_src . PHP_EOL);
}
fclose($fp);


En el ejemplo anterior hemos almacenado todas las urls de imagen en el archivo 'image_urls.txt'. Mas tarde podemos usar esto para descagar las imagenes con otro script, ya sea usando file_get_contents() o usando curl. Hemos visto un ejemplo usando file_get_contents(), a continuacion veremos un ejemplo usando curl.

/* Leemos todo el archivo y lo ponemos dentro de un array */
$lines = file('image_urls.txt');
foreach ($lines as $line)
{
/* eliminamos cualquier caracter al final de la linea */
$url = trim($line);
/* obtenemos solo el nombre de la imagen desde la url */
$filename = basename($url);
$fp = fopen($filename, 'w');
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_TIMEOUT, 6);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_exec($ch);
fclose($fp);
}
curl_close($ch);


Normalmente cada vez que se ejecuta una sesion de curl, el contenido recuperado se muestra en consola o es almacenado en una variable. Sin embargo, existe una opcion de curl importante, CURLOPT_FILE que le permite especificar un recurso de archivo en el que el contenido recuperado se guarda en un archivo, como en nuestro ejemplo anterior. Guardar todas las urls de las imagenes en un solo archivo tiene otra ventaja importante. Puede utilizar una utilidad como WGET para recuperar las imagenes.

WGET

Wget es un programa de linea de comandos de gran alcance que recupera el contenido de los servidores web, y es parte del proyecto GNU. Su nombre se deriva de World Wide Web y get. Soporta la descarga a traves de HTTP, HTTPS y FTP. Entre otras cosas sus caracteristicas incluyen descargas recursivas, conversion de enlaces para ver paginas web HTML offline, soporte para servidores proxy, etc. Escrito en portable C, Wget puede ser facilmente instalado en cualquier sistema UNIX y ha sido portado a muchos entornos, incluyendo Microsoft Windows, MAC OS X. Si utiliza windows se puede bajar la version del siguiente link. http://gnuwin32.sourceforge.net/packages/wget.htm.

Una vez que haya instalado Wget, puede pedir las imagenes del archivo image_urls.txt. Wget ahora abrira el archivo image_urls.txt, devolvera y guardara las imagenes en un directorio especificado o en el directorio actual.

d:\wget>wget -i image_urls.txt

Wget es una herramienta potente y flexible que puede ayudarle en sus esfuerzos de web scraping. Contiene un monton de opciones que le ayudaran enormemente si usted lee la documentacion cuidadosamente y se familiariza con innumerables caracteristicas. El siguiente es el formato general de wget.

wget [opcion]... [URL]...

Unos pocos ejemplos de wget se dan a continuacion.

#Descarga la portada de ejemplo.com a un archivo
#Llamado index.html
wget http://www.ejemplo.com/

#Descarga todo el contenido de ejemplo.com
wget -r -l 0 http://www.ejemplo.com/

#Descarga la portada de ejemplo.com, junto 
#Con las imagenes y hojas de estilos necesarios
#Para mostrar la pagina, y convertir las urls en interior
#Para referirse a los contenidos disponibles a nivel local
wget -p -k http://www.ejemplo.com/


Para mas informacion: wget --help

4 comentarios:

  1. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  2. Hola Arthusu, excelente tu aporte. Yo soy muy básico en programación y quiero saber como se hace para buscar una palabra en una web y recuperar la línea completa.
    Ej. en http://www.onemi.cl/alertas/
    quiero buscar la palabra "ovalle" y que me muestre esa alerta tal como aparece en esa página.
    Me podrás ayudar porfa?

    Saludos

    ResponderEliminar
  3. Hola Arthusu, gracias por este tremendo aporte, te cuento que necesito extraer un contenido de una web, pero esta no me lo entrega, no se que pasa, es un producto de amazon, quiero extraer el precio del producto, de cualquiera, no soy programador, estoy aprendiendo solo para este proyecto y tengo algunas nociones, pero esto me lleva ya casi un mes, te agradezco muchísimo que pudieras contactarme para brindarme apoyo. Muchas gracias una vez mas.

    ResponderEliminar