Web Scraping con un navegador sin cabeza: un tutorial de titiritero

Publicado: 2022-03-11

En este artículo, veremos lo fácil que es realizar web scraping (automatización web) con el método poco tradicional de usar un navegador sin cabeza .

¿Qué es un navegador sin cabeza y por qué es necesario?

En los últimos años, la web ha evolucionado desde sitios web simplistas creados con HTML y CSS básicos. Ahora hay muchas más aplicaciones web interactivas con hermosas interfaces de usuario, que a menudo se crean con marcos como Angular o React. En otras palabras, hoy en día JavaScript gobierna la web, incluyendo casi todo con lo que interactúas en los sitios web.

Para nuestros propósitos, JavaScript es un lenguaje del lado del cliente. El servidor devuelve archivos JavaScript o scripts inyectados en una respuesta HTML y el navegador los procesa. Ahora, esto es un problema si estamos haciendo algún tipo de raspado web o automatización web porque la mayoría de las veces, el contenido que nos gustaría ver o raspar en realidad se procesa mediante código JavaScript y no es accesible desde la respuesta HTML sin procesar. que entrega el servidor.

Como mencionamos anteriormente, los navegadores saben cómo procesar el JavaScript y mostrar hermosas páginas web. Ahora, ¿qué pasaría si pudiéramos aprovechar esta funcionalidad para nuestras necesidades de raspado y tuviéramos una forma de controlar los navegadores mediante programación? ¡Ahí es exactamente donde interviene la automatización del navegador sin cabeza!

¿Sin cabeza? ¿Discúlpame? Sí, esto solo significa que no hay una interfaz gráfica de usuario (GUI). En lugar de interactuar con los elementos visuales como lo haría normalmente, por ejemplo, con un mouse o un dispositivo táctil, automatiza los casos de uso con una interfaz de línea de comandos (CLI).

Chrome sin cabeza y titiritero

Hay muchas herramientas de raspado web que se pueden usar para la navegación sin cabeza, como Zombie.js o Firefox sin cabeza usando Selenium. Pero hoy exploraremos Chrome sin cabeza a través de Puppeteer, ya que es un reproductor relativamente nuevo, lanzado a principios de 2018. Nota del editor: Vale la pena mencionar el Navegador remoto de Intoli, otro reproductor nuevo, pero eso tendrá que ser un tema para otro artículo.

¿Qué es exactamente Titiritero? Es una biblioteca de Node.js que proporciona una API de alto nivel para controlar Chrome o Chromium sin interfaz gráfica o para interactuar con el protocolo DevTools. Lo mantiene el equipo de Chrome DevTools y una increíble comunidad de código abierto.

¡Basta de hablar! ¡Pasemos al código y exploremos el mundo de cómo automatizar el web scraping usando la navegación sin cabeza de Puppeteer!

Preparando el Ambiente

En primer lugar, deberá tener Node.js 8+ instalado en su máquina. Puede instalarlo aquí, o si es un amante de CLI como yo y le gusta trabajar en Ubuntu, siga estos comandos:

 curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - sudo apt-get install -y nodejs

También necesitará algunos paquetes que pueden o no estar disponibles en su sistema. Solo para estar seguro, intente instalarlos:

 sudo apt-get install -yq --no-install-recommends libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 libnss3

Configurar Headless Chrome y Titiritero

Recomendaría instalar Puppeteer con npm , ya que también incluirá la versión estable y actualizada de Chromium que está garantizada para funcionar con la biblioteca.

Ejecute este comando en el directorio raíz de su proyecto:

 npm i puppeteer --save

Nota: Esto puede llevar un tiempo, ya que Puppeteer deberá descargar e instalar Chromium en segundo plano.

Bien, ahora que estamos listos y configurados, ¡que comience la diversión!

Uso de la API de Puppeteer para raspado web automatizado

Comencemos nuestro tutorial Titiritero con un ejemplo básico. Escribiremos un script que hará que nuestro navegador sin cabeza tome una captura de pantalla de un sitio web de nuestra elección.

Cree un nuevo archivo en el directorio de su proyecto llamado screenshot.js y ábralo en su editor de código favorito.

Primero, importemos la biblioteca Puppeteer en su secuencia de comandos:

 const puppeteer = require('puppeteer');

A continuación, tomemos la URL de los argumentos de la línea de comandos:

 const url = process.argv[2]; if (!url) { throw "Please provide a URL as the first argument"; }

Ahora, debemos tener en cuenta que Puppeteer es una biblioteca basada en promesas: realiza llamadas asincrónicas a la instancia de Chrome sin cabeza debajo del capó. Mantengamos el código limpio usando async/await. Para eso, primero debemos definir una función async y poner todo el código de Puppeteer allí:

 async function run () { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); await page.screenshot({path: 'screenshot.png'}); browser.close(); } run();

En conjunto, el código final se ve así:

 const puppeteer = require('puppeteer'); const url = process.argv[2]; if (!url) { throw "Please provide URL as a first argument"; } async function run () { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); await page.screenshot({path: 'screenshot.png'}); browser.close(); } run();

Puede ejecutarlo ejecutando el siguiente comando en el directorio raíz de su proyecto:

 node screenshot.js https://github.com

¡Espera un segundo y boom! Nuestro navegador sin cabeza acaba de crear un archivo llamado captura de screenshot.png y puede ver la página de inicio de GitHub representada en él. ¡Genial, tenemos un raspador web de Chrome en funcionamiento!

Detengámonos por un minuto y exploremos lo que sucede en nuestra función run() anterior.

Primero, lanzamos una nueva instancia de navegador sin cabeza, luego abrimos una nueva página (pestaña) y navegamos a la URL proporcionada en el argumento de la línea de comandos. Por último, usamos el método incorporado de Puppeteer para tomar una captura de pantalla, y solo necesitamos proporcionar la ruta donde se debe guardar. También debemos asegurarnos de cerrar el navegador sin cabeza una vez que hayamos terminado con nuestra automatización.

Ahora que hemos cubierto los conceptos básicos, pasemos a algo un poco más complejo.

Un segundo ejemplo de raspado de titiritero

Para la siguiente parte de nuestro tutorial Titiritero, digamos que queremos raspar los artículos más nuevos de Hacker News.

Cree un nuevo archivo llamado ycombinator-scraper.js y pegue el siguiente fragmento de código:

 const puppeteer = require('puppeteer'); function run () { return new Promise(async (resolve, reject) => { try { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://news.ycombinator.com/"); let urls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }) browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run().then(console.log).catch(console.error);

Bien, hay algo más en juego aquí en comparación con el ejemplo anterior.

Lo primero que notará es que la función run() ahora devuelve una promesa, por lo que el prefijo async se ha movido a la definición de la función de promesa.

También hemos envuelto todo nuestro código en un bloque try-catch para que podamos manejar cualquier error que provoque el rechazo de nuestra promesa.

Y finalmente, estamos usando el método incorporado de Puppeteer llamado evaluate() . Este método nos permite ejecutar código JavaScript personalizado como si lo estuviéramos ejecutando en la consola de DevTools. Cualquier cosa devuelta por esa función se resuelve mediante la promesa. Este método es muy útil cuando se trata de extraer información o realizar acciones personalizadas.

El código pasado al método de evaluate() es JavaScript bastante básico que crea una serie de objetos, cada uno con campos de url y text que representan las URL de la historia que vemos en https://news.ycombinator.com/.

El resultado del script se parece a esto (pero con 30 entradas, originalmente):

 [ { url: 'https://www.nature.com/articles/d41586-018-05469-3', text: 'Bias detectives: the researchers striving to make algorithms fair' }, { url: 'https://mino-games.workable.com/jobs/415887', text: 'Mino Games Is Hiring Programmers in Montreal' }, { url: 'http://srobb.net/pf.html', text: 'A Beginner\'s Guide to Firewalling with pf' }, // ... { url: 'https://tools.ietf.org/html/rfc8439', text: 'ChaCha20 and Poly1305 for IETF Protocols' } ]

¡Bastante limpio, diría yo!

Bien, sigamos adelante. Solo tuvimos 30 artículos devueltos, mientras que hay muchos más disponibles, solo están en otras páginas. Necesitamos hacer clic en el botón "Más" para cargar la siguiente página de resultados.

Modifiquemos un poco nuestro script para agregar soporte para la paginación:

 const puppeteer = require('puppeteer'); function run (pagesToScrape) { return new Promise(async (resolve, reject) => { try { if (!pagesToScrape) { pagesToScrape = 1; } const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://news.ycombinator.com/"); let currentPage = 1; let urls = []; while (currentPage <= pagesToScrape) { let newUrls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }); urls = urls.concat(newUrls); if (currentPage < pagesToScrape) { await Promise.all([ await page.click('a.morelink'), await page.waitForSelector('a.storylink') ]) } currentPage++; } browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run(5).then(console.log).catch(console.error);

Repasemos lo que hicimos aquí:

  1. Agregamos un solo argumento llamado pagesToScrape a nuestra función principal run() . Usaremos esto para limitar cuántas páginas raspará nuestro script.
  2. Hay una nueva variable más llamada currentPage que representa el número de la página de resultados que estamos viendo actualmente. Se establece en 1 inicialmente. También envolvimos nuestra función de evaluate() en un ciclo while , de modo que siga ejecutándose mientras la currentPage sea menor o igual que pagesToScrape .
  3. Agregamos el bloque para pasar a una nueva página y esperar a que la página se cargue antes de reiniciar el ciclo while .

Notará que usamos el método page.click() para que el navegador sin interfaz haga clic en el botón "Más". También usamos el método waitForSelector() para asegurarnos de que nuestra lógica esté en pausa hasta que se cargue el contenido de la página.

Ambos son métodos API de Puppeteer de alto nivel listos para usar de inmediato.

Uno de los problemas que probablemente encontrará durante el raspado con Puppeteer es esperar a que se cargue una página. Hacker News tiene una estructura relativamente simple y fue bastante fácil esperar a que se completara la carga de la página. Para casos de uso más complejos, Puppeteer ofrece una amplia gama de funciones integradas, que puede explorar en la documentación de la API en GitHub.

Todo esto está muy bien, pero nuestro tutorial de Titiritero aún no ha cubierto la optimización. Veamos cómo podemos hacer que Titiritero corra más rápido.

Optimizando nuestro guion de titiritero

La idea general es no dejar que el navegador sin cabeza haga ningún trabajo extra. Esto podría incluir cargar imágenes, aplicar reglas CSS, activar solicitudes XHR, etc.

Al igual que con otras herramientas, la optimización de Puppeteer depende del caso de uso exacto, así que tenga en cuenta que algunas de estas ideas pueden no ser adecuadas para su proyecto. Por ejemplo, si hubiéramos evitado cargar imágenes en nuestro primer ejemplo, nuestra captura de pantalla podría no haberse visto como queríamos.

De todos modos, estas optimizaciones se pueden lograr almacenando en caché los activos en la primera solicitud o cancelando las solicitudes HTTP directamente a medida que las inicia el sitio web.

Primero veamos cómo funciona el almacenamiento en caché.

Debe tener en cuenta que cuando inicia una nueva instancia de navegador sin cabeza, Puppeteer crea un directorio temporal para su perfil. Se elimina cuando se cierra el navegador y no está disponible para su uso cuando inicia una nueva instancia; por lo tanto, ya no se podrá acceder a todas las imágenes, CSS, cookies y otros objetos almacenados.

Podemos obligar a Puppeteer a usar una ruta personalizada para almacenar datos como cookies y caché, que se reutilizarán cada vez que lo ejecutemos nuevamente, hasta que caduquen o se eliminen manualmente.

 const browser = await puppeteer.launch({ userDataDir: './data', });

Esto debería darnos un buen aumento en el rendimiento, ya que una gran cantidad de CSS e imágenes se almacenarán en caché en el directorio de datos en la primera solicitud, y Chrome no necesitará descargarlos una y otra vez.

Sin embargo, esos activos se seguirán utilizando al renderizar la página. En nuestras necesidades de extracción de artículos de noticias de Y Combinator, realmente no necesitamos preocuparnos por ningún elemento visual, incluidas las imágenes. Solo nos preocupamos por la salida HTML simple, así que intentemos bloquear todas las solicitudes.

Afortunadamente, es muy bueno trabajar con Puppeteer, en este caso, porque viene con soporte para ganchos personalizados. Podemos proporcionar un interceptor en cada solicitud y cancelar las que realmente no necesitamos.

El interceptor se puede definir de la siguiente manera:

 await page.setRequestInterception(true); page.on('request', (request) => { if (request.resourceType() === 'document') { request.continue(); } else { request.abort(); } });

Como puede ver, tenemos control total sobre las solicitudes que se inician. Podemos escribir una lógica personalizada para permitir o anular solicitudes específicas en función de su tipo de resourceType . También tenemos acceso a muchos otros datos, como request.url , por lo que podemos bloquear solo URL específicas si lo deseamos.

En el ejemplo anterior, solo permitimos que las solicitudes con el tipo de recurso "document" pasen por nuestro filtro, lo que significa que bloquearemos todas las imágenes, CSS y todo lo demás además de la respuesta HTML original.

Aquí está nuestro código final:

 const puppeteer = require('puppeteer'); function run (pagesToScrape) { return new Promise(async (resolve, reject) => { try { if (!pagesToScrape) { pagesToScrape = 1; } const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.setRequestInterception(true); page.on('request', (request) => { if (request.resourceType() === 'document') { request.continue(); } else { request.abort(); } }); await page.goto("https://news.ycombinator.com/"); let currentPage = 1; let urls = []; while (currentPage <= pagesToScrape) { await page.waitForSelector('a.storylink'); let newUrls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }); urls = urls.concat(newUrls); if (currentPage < pagesToScrape) { await Promise.all([ await page.waitForSelector('a.morelink'), await page.click('a.morelink'), await page.waitForSelector('a.storylink') ]) } currentPage++; } browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run(5).then(console.log).catch(console.error);

Manténgase seguro con los límites de tarifas

Los navegadores sin cabeza son herramientas muy poderosas. Pueden realizar casi cualquier tipo de tarea de automatización web, y Puppeteer lo hace aún más fácil. A pesar de todas las posibilidades, debemos cumplir con los términos de servicio de un sitio web para asegurarnos de no abusar del sistema.

Dado que este aspecto está más relacionado con la arquitectura, no lo cubriré en profundidad en este tutorial de Puppeteer. Dicho esto, la forma más básica de ralentizar un script de Puppeteer es agregarle un comando de suspensión:

js await page.waitFor(5000);

Esta declaración obligará a su secuencia de comandos a dormir durante cinco segundos (5000 ms). Puede poner esto en cualquier lugar antes de browser.close() .

Al igual que limitar el uso de servicios de terceros, existen muchas otras formas más sólidas de controlar el uso de Puppeteer. Un ejemplo sería construir un sistema de colas con un número limitado de trabajadores. Cada vez que quiera usar Puppeteer, colocará una nueva tarea en la cola, pero solo habrá un número limitado de trabajadores capaces de trabajar en las tareas en ella. Esta es una práctica bastante común cuando se trata de límites de tasa de API de terceros y también se puede aplicar al raspado de datos web de Puppeteer.

El lugar del titiritero en la web de rápido movimiento

En este tutorial de Puppeteer, he demostrado su funcionalidad básica como herramienta de web-scraping. Sin embargo, tiene casos de uso mucho más amplios, que incluyen pruebas de navegador sin cabeza, generación de PDF y monitoreo de rendimiento, entre muchos otros.

Las tecnologías web están avanzando rápidamente. Algunos sitios web dependen tanto de la representación de JavaScript que se ha vuelto casi imposible ejecutar solicitudes HTTP simples para rasparlos o realizar algún tipo de automatización. Afortunadamente, los navegadores autónomos son cada vez más accesibles para manejar todas nuestras necesidades de automatización, ¡gracias a proyectos como Puppeteer y los increíbles equipos detrás de ellos!