1. Code
  2. JavaScript
  3. Node

Screen Scraping con Node.js

Scroll to top

Spanish (Español) translation by Andrea Jiménez (you can also view the original English article)

Es posible que hayas utilizado NodeJS como servidor web, pero ¿sabías que también puedes usarlo para la extracción de datos de la web (web scraping)? En este tutorial, revisaremos cómo extraer páginas web estáticas, y aquellas molestas con contenido dinámico, con la ayuda de NodeJS y algunos módulos útiles de NPM.



Un poco sobre el web scraping

El web scraping siempre ha tenido una connotación negativa en el mundo del desarrollo web, y por una buena razón. En el desarrollo moderno, las API están presentes para los servicios más populares y deben usarse para recuperar datos en lugar de extraer. El problema inherente con la extracción es que se basa en la estructura visual de la página que se está extrayendo. Siempre que ese HTML cambia, no importa cuán pequeño sea el cambio, puede romper completamente tu código.

A pesar de estos defectos, es importante aprender un poco sobre el web scraping y algunas de las herramientas disponibles para ayudar con esta tarea. Cuando un sitio no revela una API o ninguna fuente de distribución (RSS/Atom, entre otras), la única opción que nos queda para obtener ese contenido... es extraer datos.

Nota: Si no puedes obtener la información que necesitas a través de una API o una fuente, es una buena señal de que el propietario no quiere que esa información sea accesible. Sin embargo, hay excepciones.


¿Por qué usar NodeJS?

Los scrapers se pueden escribir en cualquier idioma, en realidad. La razón por la que disfruto usando Node es por su naturaleza asincrónica, lo que significa que mi código no está bloqueado en ningún momento del proceso. Estoy bastante familiarizado con JavaScript, así que es un bono adicional. Finalmente, hay algunos módulos nuevos que se han escrito para NodeJS que facilitan la extracción de sitios web de una manera confiable (bueno, ¡tan confiable como el scraping puede ser!). ¡Empecemos!


Extracción simple con YQL

Comencemos con el caso de uso simple: páginas web estáticas. Estas son tus páginas web estándar y corriente. Para estos, Yahoo! Query Language (YQL) debería funcionar muy bien. Para aquellos que no están familiarizados con YQL, es una sintaxis similar a SQL que se puede usar para trabajar con diferentes API de una manera coherente.

YQL tiene algunas tablas excelentes para ayudar a los desarrolladores a obtener HTML de una página. Los que quiero destacar son:

Repasemos cada uno de ellos y revisemos cómo implementarlos en NodeJS.

tabla html

La tabla html es la forma más básica de extraer HTML de una dirección URL. Una consulta regular que usa esta tabla se ve así:

1
    select * from html where url="http://finance.yahoo.com/q?s=yhoo" and xpath='//div[@id="yfi_headlines"]/div[2]/ul/li/a'
2
  

Esta consulta consta de dos parámetros: la "url" y la "xpath". La URL se explica por sí misma. El XPath consiste en una cadena XPath que le dice a YQL qué sección del HTML se debe devolver. Prueba esta consulta aquí.

Los parámetros adicionales que puedes usar incluyen el browser (booleano), charset (cadena) y compat (cadena). No he tenido que usar estos parámetros, pero consulta la documentación si tienes necesidades específicas.

¿No te sientes cómodo con XPath?

Desafortunadamente, XPath no es una forma muy popular de recorrer la estructura del árbol HTML. Puede ser complicado leer y escribir para principiantes.

Echemos un vistazo a la siguiente tabla, que hace lo mismo, pero le permite utilizar CSS en su lugar

tabla data.html.cssselect

La tabla data.html.cssselect es mi forma preferida de extraer HTML de una página. Funciona de la misma manera que la tabla html pero te permite usar CSS en lugar de XPath. En la práctica, esta tabla convierte el CSS a XPath y luego llama a la tabla html, por lo que es un poco más lento. La diferencia debería ser insignificante para las necesidades de extracción.

Una consulta regular que usa esta tabla se ve así:

1
    select * from data.html.cssselect where url="www.yahoo.com" and css="#news a"
2
  

Como puedes ver, es mucho más limpio. Te recomiendo que pruebes este método primero cuando intentes extraer HTML usando YQL. Prueba esta consulta aquí.

tabla htmlstring

La tabla htmlstring es útil para los casos en los que estás intentando extraer un gran fragmento de texto con formato de una página web.

El uso de esta tabla te permite recuperar todo el contenido HTML de esa página en una sola cadena, en lugar de como JSON que se divide en función de la estructura DOM.

Por ejemplo, una respuesta JSON normal que extrae una etiqueta <a> se ve así:

1
    "results": { "a": { "href": "...", "target": "_blank", "content": "Apple Chief Executive Cook To Climb on a New Stage" } }
2
  

¿Ves cómo se definen los atributos como propiedades? En su lugar, la respuesta de la tabla htmlstring tendría este aspecto:

1
    "results": { "result": { "<a href=\"…\" target="_blank">Apple Chief Executive Cook To Climb on a New Stage</a> } }
2
  

Entonces, ¿por qué lo usarías? Bueno, según mi experiencia, esto es de gran utilidad cuando intentas extraer una gran cantidad de texto formateado. Por ejemplo, ten en cuenta el siguiente fragmento de código:

1
    <p>Lorem ipsum <strong>dolor sit amet</strong>, consectetur adipiscing elit.</p> <p>Proin nec diam magna. Sed non lorem a nisi porttitor pharetra et non arcu.</p>
2
  

Al usar la tabla htmlstring, puedes obtener este HTML como una cadena y usar expresiones regulares para eliminar las etiquetas HTML, lo que te deja solo con el texto. Esta es una tarea más fácil que iterar a través de JSON que se divide en propiedades y objetos secundarios según la estructura DOM de la página.


Uso de YQL con NodeJS

Ahora que sabemos un poco sobre algunas de las tablas disponibles para nosotros en YQL, implementemos un scraper web usando YQL y NodeJS. Afortunadamente, esto es muy simple, gracias al módulo node-yql de Derek Gathright.

Podemos instalar el módulo usando npm:

1
    npm install yql
2
  

El módulo es extremadamente simple y consta de un solo método: el método YQL.exec(). Se define como lo siguiente:

1
    function exec (string query [, function callback] [, object params] [, object httpOptions])
2
  

Podemos usarlo requiriéndolo y llamando a YQL.exec(). Por ejemplo, supongamos que queremos extraer los titulares de todas las publicaciones de la página principal de Nettuts:

1
    var YQL = require("yql"); new YQL.exec('select * from data.html.cssselect where url="http://net.tutsplus.com/" and css=".post_title a"', function(response) { //response consists of JSON that you can parse });
2
  

Lo mejor de YQL es su capacidad para probar sus consultas y determinar qué está obteniendo JSON en tiempo real. Ve a la consola para probar esta consulta, o haz clic aquí para ver el JSON sin procesar.

Los objetos params y httpOptions son opcionales. Los parámetros pueden contener propiedades como env (ya sea que estés utilizando un entorno específico para las tablas) y format (xml o json). Todas las propiedades que se pasan a params están codificadas en URI y se añaden a la cadena de consulta. El objeto httpOptions se pasa al encabezado de la solicitud. Aquí puedes especificar si quieres habilitar SSL, por ejemplo.

El archivo JavaScript, denominado yqlServer.js, contiene el código mínimo necesario para extraer con YQL. Puedes ejecutarlo emitiendo el siguiente comando en tu terminal:

1
    node yqlServer.js
2
  

Excepciones y otras herramientas notables

YQL es mi opción preferida para eliminar contenido de páginas web estáticas, porque es fácil de leer y de usar. Sin embargo, YQL fallará si la página web en cuestión tiene un archivo robots.txt que niega una respuesta. En este caso, puedes ver algunas de las utilidades que se mencionan a continuación, o usar PhantomJS, del que hablaremos en la siguiente sección.

Node.io es una utilidad de nodo útil diseñada específicamente para la extracción de datos. Puedes crear trabajos que tomen entradas, las procesen y devuelvan alguna salida. Node.io está bien visto en Github y tiene algunos ejemplos útiles para comenzar.

JSDOM es un proyecto muy popular que implementa el DOM W3C en JavaScript. Cuando se proporciona HTML, puede construir un DOM con el que puede interactuar. Consulta la documentación para ver cómo puedes usar JSDOM y cualquier biblioteca JS (como jQuery) juntos para extraer datos de páginas web.


Scraping de páginas con contenido dinámico

Hasta ahora, hemos analizado algunas herramientas que pueden ayudarnos a extraer páginas web con contenido estático. Con YQL, es relativamente fácil. Desafortunadamente, a menudo se nos presentan páginas que tienen contenido que se carga dinámicamente con JavaScript. En estos casos, la página suele estar vacía inicialmente y luego el contenido se agrega después. ¿Cómo podemos lidiar con este problema?

Un ejemplo

Permíteme dar un ejemplo de lo que quiero decir; subí un archivo HTML simple a mi propio sitio web, que agrega contenido, a través de JavaScript, dos segundos después de que la función document.ready() sea llamada. Puedes consultar la página aquí. Así es como se ve la fuente:

1
    <!DOCTYPE html> <html> <head> <title>Test Page with content appended after page load</title> </head> <body> Content on this page is appended to the DOM after the page is loaded. <div id="content"> </div> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script> <script> $(document).ready(function() { setTimeout(function() { $('#content').append("<h2>Article 1</h2><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p><h2>Article 2</h2><p>Ut sed nulla turpis, in faucibus ante. Vivamus ut malesuada est. Curabitur vel enim eget purus pharetra tempor id in tellus.</p><h2>Article 3</h2><p>Curabitur euismod hendrerit quam ut euismod. Ut leo sem, viverra nec gravida nec, tristique nec arcu.</p>"); }, 2000); }); </script> </body> </html>
2
  

Ahora, trataremos de extraer el texto dentro de <div id="content"> usando YQL.

1
    var YQL = require("yql"); new YQL.exec('select * from data.html.cssselect where url="http://tilomitra.com/repository/screenscrape/ajax.html" and css="#content"', function(response) { //This will return undefined! The scraping was unsuccessful! console.log(response.results); });
2
  

Notarás que YQL devuelve undefined porque, cuando se carga la página, el <div id="content"> está vacío. El contenido aún no se ha anexado. Puedes probar la consulta por sí mismo aquí.

¡Veamos cómo podemos evitar este problema!

Ingresa PhantomJS

PhantomJS puede cargar páginas web e imitar un navegador basado en Webkit sin la GUI.

Mi método preferido para extraer información de estos sitios es usar PhantomJS. PhantomJS se describe a sí mismo como un "Webkit sin cabeza con una API de JavaScript". En términos simplistas, esto significa que PhantomJS puede cargar páginas web e imitar un navegador basado en Webkit sin la GUI. Como desarrollador, podemos recurrir a métodos específicos que proporciona PhantomJS para ejecutar código en la página. Dado que se comporta como un navegador, los scripts de la página web se ejecutan como lo harían en un navegador normal.

Para obtener datos de nuestra página, vamos a utilizar PhantomJS-Node, un pequeño proyecto de código abierto que une PhantomJS con NodeJS. En esencia, este módulo ejecuta PhantomJS como un proceso secundario.

Instalación de PhantomJS

Antes de poder instalar el módulo NPM PhantomJS-Node, debes instalar PhantomJS. Sin embargo, instalar y compilar PhantomJS puede ser un poco complicado.

Primero, dirígete a PhantomJS.org y descarga la versión apropiada para tu sistema operativo. En mi caso, fue Mac OSX.

Después de descargarlo, descomprímelo en algún lugar como /Applications/. Luego, quieres agregarlo a tu PATH:

1
    sudo ln -s /Applications/phantomjs-1.5.0/bin/phantomjs /usr/local/bin/
2
  

Reemplaza 1.5.0 con tu versión descargada de PhantomJS. Ten en cuenta que no todos los sistemas tendrán /usr/local/bin/. Algunos sistemas tendrán: /usr/bin/, /bin/, o usr/X11/bin en su lugar.

Para usuarios de Windows, consulta el breve tutorial aquí. Sabrás que todo está configurado cuando abras tu Terminal y escribas phantomjs, y no obtengas ningún error.

Si te sientes incómodo editando tu PATH, toma nota de dónde descomprimiste PhantomJS y te mostraré otra forma de configurarlo en la siguiente sección, aunque te recomiendo que edites tu PATH.

Instalación de PhantomJS-Node

Configurar PhantomJS-Node es mucho más fácil. Siempre que tengas NodeJS instalado, puedes instalarlo a través de npm:

1
    npm install phantom
2
  

Si no editaste tu PATH en el paso anterior al instalar PhantomJS, puedes ir al directorio phantom/ desplegado por npm y editar esta línea en phantom.js.

1
    ps = child.spawn('phantomjs', args.concat([__dirname + '/shim.js', port]));
2
  

Cambia la ruta a:

1
    ps = child.spawn('/path/to/phantomjs-1.5.0/bin/phantomjs', args.concat([__dirname + '/shim.js', port]));
2
  

Una vez hecho esto, puedes probarlo ejecutando este código:

1
    var phantom = require('phantom'); phantom.create(function(ph) { return ph.createPage(function(page) { return page.open("http://www.google.com", function(status) { console.log("opened google? ", status); return page.evaluate((function() { return document.title; }), function(result) { console.log('Page title is ' + result); return ph.exit(); }); }); }); });
2
  

Al ejecutar esto en la línea de comandos, debería aparecer lo siguiente:

1
    opened google? success Page title is Google
2
  

Si tienes esto, está todo listo y estás listo para comenzar. Si no, ¡publica un comentario e intentaré ayudarte!

Uso de PhantomJS-Node

Para que sea más fácil para ti, incluí un archivo JS, llamado phantomServer.js en la descarga, que usa parte de la API de PhantomJS para cargar una página web. Espera 5 segundos antes de ejecutar el JavaScript que extrae la página. Puedes ejecutarlo navegando al directorio y emitiendo el siguiente comando en tu terminal:

1
    node phantomServer.js
2
  

Daré una descripción general de cómo funciona aquí. Primero, necesitamos PhantomJS:

1
    var phantom = require('phantom');
2
  

Luego, implementamos algunos métodos de la API. Es decir, creamos una instancia de página y luego llamamos al método open():

1
    phantom.create(function(ph) { return ph.createPage(function(page) { //From here on in, we can use PhantomJS' API methods return page.open("http://tilomitra.com/repository/screenscrape/ajax.html", function(status) { //The page is now open console.log("opened site? ", status); }); }); });
2
  

Una vez abierta la página, podemos insertar algunos JavaScript en la página. Inyectemos jQuery a través del método page.injectJs() :

1
    phantom.create(function(ph) { return ph.createPage(function(page) { return page.open("http://tilomitra.com/repository/screenscrape/ajax.html", function(status) { console.log("opened site? ", status); page.injectJs('http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js', function() { //jQuery Loaded //We can use things like $("body").html() in here. }); }); }); });
2
  

jQuery ahora está cargado, pero aún no sabemos si el contenido dinámico de la página se cargó. Para tener en cuenta esto, normalmente pongo mi código de extracción dentro de una función setTimeout() que se ejecuta después de un cierto intervalo de tiempo. Si quieres una solución más dinámica, la API de PhantomJS te permite escuchar y emular determinados eventos. Vayamos con el caso simple:

1
    setTimeout(function() { return page.evaluate(function() { //Get what you want from the page using jQuery. //A good way is to populate an object with all the jQuery commands that you need and then return the object. var h2Arr = [], //array that holds all html for h2 elements pArr = []; //array that holds all html for p elements //Populate the two arrays $('h2').each(function() { h2Arr.push($(this).html()); }); $('p').each(function() { pArr.push($(this).html()); }); //Return this data return { h2: h2Arr, p: pArr } }, function(result) { console.log(result); //Log out the data. ph.exit(); }); }, 5000);
2
  

Poniéndolo todo junto, nuestro archivo phantomServer.js se ve así:

1
    var phantom = require('phantom'); phantom.create(function(ph) { return ph.createPage(function(page) { return page.open("http://tilomitra.com/repository/screenscrape/ajax.html", function(status) { console.log("opened site? ", status); page.injectJs('http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js', function() { //jQuery Loaded. //Wait for a bit for AJAX content to load on the page. Here, we are waiting 5 seconds. setTimeout(function() { return page.evaluate(function() { //Get what you want from the page using jQuery. A good way is to populate an object with all the jQuery commands that you need and then return the object. var h2Arr = [], pArr = []; $('h2').each(function() { h2Arr.push($(this).html()); }); $('p').each(function() { pArr.push($(this).html()); }); return { h2: h2Arr, p: pArr }; }, function(result) { console.log(result); ph.exit(); }); }, 5000); }); }); }); });
2
  

Esta implementación es un poco ordinaria y desorganizada, pero es clara. ¡Usando PhantomJS, podemos extraer una página que tiene contenido dinámico! Tu consola debería generar lo siguiente:

1
    → node phantomServer.js opened site? success { h2: [ 'Article 1', 'Article 2', 'Article 3' ], p: [ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 'Ut sed nulla turpis, in faucibus ante. Vivamus ut malesuada est. Curabitur vel enim eget purus pharetra tempor id in tellus.', 'Curabitur euismod hendrerit quam ut euismod. Ut leo sem, viverra nec gravida nec, tristique nec arcu.' ] }
2
  

Conclusión

En este tutorial, revisamos dos formas diferentes de realizar web scraping. Si extraemos de una página web estática, podemos aprovechar YQL, que es fácil de configurar y usar. Por otro lado, para sitios dinámicos, podemos aprovechar PhantomJS. Es un poco más difícil de configurar, pero ofrece más capacidades. Recuerda: ¡también puedes usar PhantomJS en sitios estáticos!

Si tienes alguna pregunta sobre este tema, no dudes en preguntar en la parte de abajo y haré todo lo posible para ayudarte.