Creando un scraper – Parte 3: Scraper en NodeJS
En esta entrada de título redundante vamos a ver como sacar información de una página web en NodeJS, y lo vamos a hacer de distintas formas. Para ello vamos a definir la estructura de un scraper, y realizaremos dos implementaciones, la primera basada en expresiones regulares, y la segunda basada en cheerio.
Nuestro primer paso va a ser definir una estructura común para los scrapers que creemos. Puede que no sea necesario en un futuro, pero no cuesta nada ser un poco ordenado, y de esta manera si mas adelante queremos tener un sistema complejo de crawling y scraping, podremos jugar fácilmente con una batería de scrapers.
Por lo tanto, nuestros scrapers extenderán a un scraper básico, en el que definiremos cómo descargar una página web, y se diferenciarán en la manera por la cual extraen la información de la página.
Este es el código del scraper básico, lo comento a continuación aunque es casi auto-explicativo:
const request = require('request'); var Scraper = function (options) { this.options = options; } Scraper.prototype.scrap = function (url, callback) { if (!callback) return new Promise((resolve, reject) => { this._get(url, (err, resp, body) => { if (err) { reject(err); } else { resolve(this._scrap(body)); } }) }) else { this._get(url, (err, resp, body) => { callback(err, this._scrap(body)); }) } } Scraper.prototype._get = function (url, callback) { request.get(url, this.options, callback); } Scraper.prototype._scrap = function (content) { return content; } module.exports = Scraper;
La estructura consta de una función «pública», scrap, que acepta por parámetros una url, la cual descargar, y un callback opcional. Esta función devuelve los datos extraídos mediante el callback, si se define, o mediante una promesa. De esta manera, desde nuestro código principal, solo tendremos que llamar a la función scrap con una url y «esperar» a que lleguen los datos.
Entre bambalinas tenemos dos funciones. _get, que se encarga de descargar la página web mediante requestjs, y _scrap, que es dónde procesaremos el contenido de la propia página para obtener la información que deseamos.
Se puede observar que el Scraper tiene un constructor que acepta un objeto options, y que es el mismo que se emplea en la petición mediante requestjs. Estas opciones las usaremos para, entre otras cosas, añadir las cabeceras de las peticiones HTTP, pero lo veremos mas adelante.
Para poner en práctica nuestro programa, vamos a definir los dos scrapers que hemos mencionado antes, para conseguir extraer los títulos y enlaces de las entradas de este blog que aparecen en la página principal, mediante regex y cheerio.
Scraper con regex
Si analizamos el código de la página principal de este blog, podemos observar que podemos obtener el título de las entradas y sus urls fijándonos en los elementos h2 con clase entry-title, como el siguiente:
<h2 class="entry-title"> <a href="https://jrubia.com/es/2018/05/05/creando-un-scraper-parte-2-eligiendo-tecnologia-y-definiendo-la-estructura/" title="Creando un scraper – Parte 2: eligiendo tecnología y definiendo la estructura">Creando un scraper – Parte 2: eligiendo tecnología y definiendo la estructura</a> </h2>
Con la siguiente expresión regular podemos capturar tanto la url de la entrada, en el primer grupo de captura, como el título de la misma, en el segundo grupo
/class="entry-title">[.\s]*<a href="([^"]*)".*>([^<]*)<\/a>/ig
De esta manera, iterando sobre todos los matches de la regex sobre el código HTML de la página principal, obtendremos todas las entradas que se publican en la misma. Para esto vamos a crear un scraper nuevo, basado en el anterior, y sustituyendo la funcion _scrap para encontrar la información de las entradas.
El código es el siguiente.
const baseScrapper = require('./scraper'); const regex = /class="entry-title">[.\s]*<a href="([^"]*)".*>([^<]*)<\/a>/ig class regexScraper extends baseScrapper {} regexScraper.prototype._scrap = function (content) { console.log("regex"); var entries = []; var m; do { m = regex.exec(content); if (m) { entries.push({ url: m[1], title: m[2] }) } } while (m); return entries; } module.exports = regexScraper;
De esta manera, una vez el scraper se descargue la página web y encuentre mediante la expresión regular anterior toda la información buscada, nos la devolverá en un array de elementos, ya sea mediante callback o promesas.
Scraper con cheerio
Cheerio es una implementación del núcleo de jQuery diseñada para el servidor, de esta manera podemos emplear los selectores CSS y las funciones de jQuery para buscar los elementos a cuyo padre tiene la clase entry-title y obtener el valor del atributo href para sacar la url, y el contenido de las etiquetas para conseguir el título.
const baseScraper = require('./scraper'); const cheerio = require('cheerio'); class jQueryScraper extends baseScraper {}; jQueryScraper.prototype._scrap = function (content) { console.log("jquery"); const $ = cheerio.load(content); return $(".entry-title a").map(function(i, el) { var $a = $(this); return { url: $a.attr('href'), title: $a.html() } }).get() } module.exports = jQueryScraper;
Probando los scrapers
Como podemos observar el código de ambos scrapers es muy sencillo y solo nos queda poder probarlos. Para ello vamos a crear una instancia de cada uno de ellos y lanzarlos contra la url de este blog, https://jrubia.com.
var regexScraper = require('./scrapers/regexScraper'); var jQueryScraper = require('./scrapers/jQueryScraper'); var rg = new regexScraper({ headers: { 'User-Agent': 'ScraperBot/0.1' } }); var jq = new jQueryScraper({ headers: { 'User-Agent': 'ScraperBot/0.1' } }); rg.scrap("https://jrubia.com").then((data) => { console.log("Data from regex: ", JSON.stringify(data)); }).catch((err) => { console.log(err); }) jq.scrap("https://jrubia.com").then((data) => { console.log("Data from jquery: ", JSON.stringify(data)); }).catch((err) => { console.log(err); })
En el código podemos observar que a los scrapers se le pasa como opciones un objeto que, una vez pasado a requestjs, define la cabecera User-Agent. Esto es así porque WordPress no admite peticiones sin esta cabecera, así que de momento nos inventamos un User-Agent, aunque mas adelante, cuando hablemos de las buenas practicas del scraping, entraremos más en detalle acerca de las cabeceras HTTP.
Todo el código está disponible en este repo de GitHub: https://github.com/fjdelarubia/NodeScraper
Thank you!!1