Scrapping la Web con apache HTTPComponents y Jsoup

Siguiendo con el tema de recuperación de información desde páginas, se supondría que en esta entrada hable un poco más de jARVEST, pero se dieron algunos cambios que hicieron que cambie de herramienta.

Tengo varios millones de URLs a procesar y para acelerar su procesamiento utilizo varios cientos de hilos – Threads que permiten mejorar la velocidad de procesamiento de horas a minutos. Y aquí el problema con jARVEST cuando traté de usarlo con más de 25 hilos empecé a tener problemas de falta de espacio de memoria (concretamente Java heap space – OutOfMemoryError), me imagino que la causa es porque jARVEST usa JRuby, aunque no lo he confirmado.

Por lo anterior dejé de lado jARVEST, y pasé a utilizar Apache HTTPComponents, aunque ya lo había usado anteriormente, no lo había explotado en temas de scrapping. HTTPComponents es un API bastante potente que se puede convertir en compleja, pero que gracias a Fluent API, la tarea se convierte en sencilla.

El código básico que usé es el siguiente:

Request.Get("https://cafelojano.wordpress.com").
userAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.76.4 (KHTML, like Gecko) Version/7.0.4 Safari/537.76.4").
 connectTimeout(10 * 1000).
 socketTimeout(15 * 1000).
 execute().returnContent().asString();

El código anterior recupera el código HTML de la página principal de este blog. De aquí comentar únicamente los métodos connectTimeout y socketTimeout que según ésta página, el primero determina el tiempo que se esperará a que el servidor responda, mientras que el segundo determina el tiempo de espera entre flujos de datos.

La mejor noticia es que con la configuración anterior, aquel link que me devolvía una página de login, ahora si me devuelve la misma página que cualquier navegador.

El siguiente paso es utilizar Jsoup para encontrar todas los metadatos que son de nuestro interés. El siguiente código lo utilicé para obtener las metatags (todas las etiquetas que empiezan con la palabra meta). Utilizo un Map para almacenar los metatags de la página, aunque este código únicamente permite obtener el último valor de la metatag, en el caso de que una misma metatag se use varias veces (dentro de la misma página, esto es posible).


Map<String, String> output = new HashMap<String, String>();
Elements metaElements = doc.select("meta");
String name = "";
String content = "";
Attributes atts;

for (Element ele : metaElements) {
   atts = ele.attributes();
   if (atts.size() > 1) {
      for (Attribute att : atts) {
         if (att.getKey().equalsIgnoreCase("content")) {
            content = att.getValue();
         } else {
            name = att.getValue();
         }
      }
   } else {
      Attribute att = atts.asList().get(0);
      name = att.getKey();
      content = att.getValue();
   }
   output.put(name.trim().toLowerCase(), content.trim());
}

En una prueba se analizó 13455 links y aquí está el top ten de los metadatos más utilizados:

Metatag Cantidad
description 9588
content-type 6556
og:url 6368
og:image 6324
og:title 6251
og:type 6245
og:description 6010
og:site_name 5809
charset 5303
viewport 4399

Como se puede ver en la tabla anterior cerca del 50% de las páginas analizadas usan Facebook Open Graph, mientras que recién en la posición 12 aparece Twitter con 4229 páginas usando twitter:card.

Pero en resumen qué hacen éstas metatags de open graph, básicamente permiten que las URLs que publicamos en Facebook aparezcan con una imagen (og:image), un título (og:title) y una descripción (og:description), así como en la siguiente imagen

Open graph, un ejemplo práctico
Un ejemplo del uso de open graph

En las próximas entradas les seguiré mostrando la implementación completa, para ver el trabajo con hilos.

Scrapping la Web con Java – Jsoup y jARVEST

Una de las tareas en las que he trabajado últimamente es recuperar información de links URLs, principalmente información que se encuentra publicada en forma de anotaciones que dentro del HTML se expresan a través de Metatags. Básicamente estoy interesado en recuperar el título de la página Web (claro que no es una metatag) y las meta «description» y «keywords«.

Hasta el momento he trabajado con Jsoup y he tenido muy buenos resultados, Jsoup es una framework que permite representar a una página HTML, que se puede leer desde una url, una variable o un archivo, como un objeto y a través de algunos métodos manipular el DOM. Otra de las características que destaco la manera de seleccionar los elementos del DOM, que es similar a jQuery.

Estas son las líneas de código que utilizo para leer una página Web desde una URL:


Document doc = Jsoup.connect(urlHome).
userAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.76.4  (KHTML, like Gecko) Version/7.0.4 Safari/537.76.4").
timeout(10 * 1000).
ignoreContentType(true).
ignoreHttpErrors(true).
followRedirects(true).
get();

Mencionar el método userAgent que le dice al servidor Web quién hace la llamada, uso el de Safari para «tratar» de converser al servidor Web que es una persona que quiere ver la página y no un programa que esta haciendo scrap. Otros métodos que mencionaré son ignoreContentType e ignoreHttpErrors, estos métodos ayudan a que no se lancen excepciones cuando la URL a visitar, no apunta a una página HTML sino a un archivo con extensión diferente a html, y que en lugar de lanzar una excepción cuando existen errores HTTP 4xx o 5xx, este error se convierta en un objeto.

Para obtener las meta, utilicé el siguiente código:


doc.title();

Elements metaElements = doc.select("meta[name]");

Así puedo obtener el título y todas las meta que tienen la propiedad name, para luego trabajar en las que son de mi interés.

Luego de hacer scrapping a varios miles de páginas, me di cuenta que muchas de ellas no poseían, ni la etiqueta title ni las meta description o keywords y que en muchos casos preferían utilizar anotaciones de Facebook Open Graph o de Twitter Cards. Así que modifiqué el código para que también seleccione estas meta y quedando así:

doc.title();

Elements metaElements = doc.select("meta[name]");
Elements others = doc.select("meta[property]");

Con esto se mejoró notablemente la recuperación de la información, pero aún quedaban algunas URL con un comportamiento extraño, por ejemplo esta: Can Emotional Intelligence Be Taught? que cuando se la abre desde cualquier navegador funciona sin problema, pero cuando la proceso con Jsoup me aparece una página de login.

El problema parece ser que ese sitio Web, recibe la solicitud, escribe algunas cookies y envía una página de redirección, recibe la segunda página y busca las cookies escritas previamente, esto es normal en navegador Web, pero no en Jsoup busqué la forma de arreglarlo, pero no tuve éxito.

Es así que recordé otro framework que permite hacer scrapping que se llama jARVEST. Estudiando la escasa documentación pude comprender el potencial de la herramienta. Mi primera prueba fue cargar el contenido de la página de ejemplo y usar Jsoup para hacer el scrap, así:

Document doc;
String url = "http://www.nytimes.com/2013/09/15/magazine/can-emotional-intelligence-be-taught.html?_r=1&";
Jarvest scrapper;
String html;

scrapper = new Jarvest();
html = scrapper.exec("wget",url)[0];

doc = Jsoup.parse(html);

System.out.println(doc.title());

Y así pude ver que ya no se devolvía el título de la página de login, sino el mismo título que muestra el navegador. El problema real con jARVEST es la falta de documentación, estuve buscando un par de días y no pude encontrar ni la Javadoc. Pero con varios intentos prueba/error pude construir este «transformador» para obtener los mismos datos que con Jsoup. Este código se puede ejecutar desde una ventana terminal.


echo "http://www.nytimes.com/2013/09/15/magazine/can-emotional-intelligence-be-taught.html?_r=1&" | ./jarvest.sh run -p "wget
branch(:BRANCH_DUPLICATED, :ORDERED){ 
 pipe{
 xpath('//title')
 }
 branch(:BRANCH_DUPLICATED, :SCATTERED){
 select(:selector=>'meta[name]', :attribute=>'name')
 select(:selector=>'meta[name]', :attribute=>'content')
 }
 branch(:BRANCH_DUPLICATED, :SCATTERED){
 select(:selector=>'meta[property]', :attribute=>'name')
 select(:selector=>'meta[property]', :attribute=>'content') 
 }
}"

En una siguiente entrada hablaré un poco más de jARVEST, su potencial y lo que he aprendido, ya que veo un potencial bastante grande en esta herramienta.