Cómo crear una API personalizada desde cualquier sitio web usando Puppeteer

A menudo sucede que te encuentras con un sitio web y te ves obligado a realizar una serie de acciones para finalmente obtener algunos datos. Entonces se enfrenta a un dilema: ¿cómo puede hacer que estos datos estén disponibles en una forma que su aplicación pueda consumir fácilmente?

El raspado viene al rescate en tal caso. Y seleccionar la herramienta adecuada para el trabajo es muy importante.

Titiritero: no solo otra biblioteca de scraping

Puppeteer es una biblioteca de Node.js mantenida por el equipo de Chrome Devtools en Google. Básicamente, ejecuta una instancia de Chromium o Chrome (quizás el nombre más reconocible) de forma sin cabeza (o configurable) y expone un conjunto de API de alto nivel.

De su documentación oficial, titiritero normalmente se aprovecha para múltiples procesos que no se limitan a lo siguiente:

  • Generación de capturas de pantalla y PDF
  • Rastrear un SPA y generar contenido renderizado previamente (es decir, renderizado del lado del servidor)
  • Probando extensiones de Chrome
  • Pruebas de automatización de interfaces web
  • Diagnóstico de problemas de rendimiento a través de técnicas como capturar el seguimiento de la línea de tiempo de un sitio web

En nuestro caso, necesitamos poder acceder a un sitio web y mapear los datos en un formulario que nuestra aplicación pueda consumir fácilmente.

¿Suena simple? La implementación tampoco es tan compleja. Empecemos.

Encadenar el código

Mi afición por los productos de Amazon me impulsa a usar una de sus páginas de listado de productos como muestra aquí. Implementaremos nuestro caso de uso en dos pasos:

  • Extraiga datos de la página y mapee en un formato JSON fácilmente consumible
  • Agregue un poco de automatización para hacer nuestras vidas un poco más fáciles

Puede encontrar el código completo en este repositorio.

Extraeremos los datos de este enlace: //www.amazon.in/s?k=Shirts&ref=nb_sb_noss_2 (una lista de las camisetas más buscadas como se muestra en la imagen) en un formato API.

Antes de empezar a utilizar el titiritero extensamente en esta sección, debemos comprender las dos clases principales que ofrece.

  • Navegador: lanza una instancia de Chrome cuando usamos puppeteer.launcho puppeteer.connect. Esto funciona como una simple emulación de navegador.
  • Página: se asemeja a una sola pestaña en un navegador Chrome. Proporciona un conjunto exhaustivo de métodos que puede usar con una instancia de página en particular y se invoca cuando llamamos browser.newPage. Al igual que puede crear varias pestañas en el navegador, también puede crear varias instancias de página a la vez en titiritero.

Configuración de Titiritero y navegación a la URL de destino

Comenzamos a configurar titiritero utilizando el módulo npm proporcionado. Después de instalar puppeteer, creamos una instancia del navegador y la clase de página y navegamos hasta la URL de destino.

const puppeteer = require('puppeteer'); const url = '//www.amazon.in/s?k=Shirts&ref=nb_sb_noss_2'; async function fetchProductList(url) { const browser = await puppeteer.launch({ headless: true, // false: enables one to view the Chrome instance in action defaultViewport: null, // (optional) useful only in non-headless mode }); const page = await browser.newPage(); await page.goto(url, { waitUntil: 'networkidle2' }); ... } fetchProductList(url); 

Usamos networkidle2como valor para la waitUntilopción mientras navegamos a la URL. Esto asegura que el estado de carga de la página se considere final cuando no tenga más de 2 conexiones ejecutándose durante al menos 500 ms.

Nota: No es necesario tener Chrome o una instancia de este instalado en su sistema para que el titiritero funcione. Ya se envía con una versión ligera incluida con la biblioteca.

Métodos de página para extraer y mapear datos

El DOM ya se ha cargado en la instancia de página creada. Seguiremos adelante y aprovecharemos el page.evaluate()método para consultar el DOM.

Antes de comenzar, debemos averiguar los puntos de datos exactos que debemos extraer. En la muestra actual, cada uno de los objetos de producto tendrá este aspecto.

{ brand: 'Brand Name', product: 'Product Name', url: '//www.amazon.in/url.of.product.com/', image: '//www.amazon.in/image.jpg', price: '₹599', }

Hemos trazado la estructura que queremos lograr. Es hora de comenzar a inspeccionar el DOM en busca de identificadores. Comprobamos los selectores que se producen en todos los elementos que se van a mapear. Utilizaremos principalmente document.querySelectory document.querySelectorAllpara atravesar el DOM.

... async function fetchProductList(url) { ... await page.waitFor('div[data-cel-widget^="search_result_"]'); const result = await page.evaluate(() => { // counts total number of products let totalSearchResults = Array.from(document.querySelectorAll('div[data-cel-widget^="search_result_"]')).length; let productsList = []; for (let i = 1; i  0 ? onlyProduct = true : emptyProductMeta = true; } let productsDetails = productNodes.map(el => el.innerText); if (!emptyProductMeta) { product.brand = onlyProduct ? '' : productsDetails[0]; product.product = onlyProduct ? productsDetails[0] : productsDetails[1]; } // traverse for product image let rawImage = document.querySelector(`div[data-cel-widget="search_result_${i}"] .s-image`); product.image =rawImage ? rawImage.src : ''; // traverse for product url let rawUrl = document.querySelector(`div[data-cel-widget="search_result_${i}"] a[target="_blank"].a-link-normal`); product.url = rawUrl ? rawUrl.href : ''; // traverse for product price let rawPrice = document.querySelector(`div[data-cel-widget="search_result_${i}"] span.a-offscreen`); product.price = rawPrice ? rawPrice.innerText : ''; if (typeof product.product !== 'undefined') { !product.product.trim() ? null : productsList = productsList.concat(product); } } return productsList; }); ... } ...

// atravesar los nombres de marcas y productos

Después de investigar el DOM, vemos que cada elemento de la lista está incluido debajo de un elemento con el selector div[data-cel-widget^="search_result_"]. Este selector en particular busca todas las divetiquetas con el atributo data-cel-widgetque tienen un valor que comienza con search_result_.

De manera similar, mapeamos los selectores para los parámetros que necesitamos como se enumeran. Si desea obtener más información sobre el recorrido DOM, puede consultar este artículo informativo de Zell.

  • total de artículos enumerados:div[data-cel-widget^="search_result_"]
  • marca:div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base ( irepresenta el número de nodo en total listed items)
  • producto:div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base  o div[data-cel-widget="search_result_${i}"] .a-size-medium.a-color-base.a-text-normal( irepresenta el número de nodo en total listed items)
  • url:div[data-cel-widget="search_result_${i}"] a[target="_blank"].a-link-normal ( irepresenta el número de nodo en total listed items)
  • imagen:div[data-cel-widget="search_result_${i}"] .s-image ( irepresenta el número de nodo en total listed items)
  • precio:div[data-cel-widget="search_result_${i}"] span.a-offscreen ( irepresenta el número de nodo en total listed items)
Nota: Esperamos que los div[data-cel-widget^="search_result_"]elementos con nombre del selector estén disponibles en la página mediante el page.waitFormétodo.

Una vez page.evaluateque se invoca el método, podemos ver los datos que necesitamos registrados.

Agregar automatización para facilitar el flujo

Hasta ahora, podemos navegar a una página, extraer los datos que necesitamos y transformarlos en un formulario listo para API. Eso suena todo bien.

Sin embargo, considere por un momento un caso en el que tenga que navegar a una URL desde otra realizando algunas acciones, y luego intente extraer los datos que necesita.

¿Eso haría tu vida un poco más complicada? De ningún modo. Titiritero puede imitar fácilmente el comportamiento del usuario. Es hora de agregar algo de automatización a nuestro caso de uso existente.

A diferencia del ejemplo anterior, iremos a la amazon.inpágina de inicio y buscaremos 'Camisetas'. Nos llevará a la página de listado de productos y podremos extraer los datos requeridos del DOM. Pan comido. Veamos el código.

... async function fetchProductList(url, searchTerm) { ... await page.goto(url, { waitUntil: 'networkidle2' }); await page.waitFor('input[name="field-keywords"]'); await page.evaluate(val => document.querySelector('input[name="field-keywords"]').value = val, searchTerm); await page.click('div.nav-search-submit.nav-sprite'); // DOM traversal and data mapping logic // returns a productsList array ... } fetchProductList('//amazon.in', 'Shirts'); 

We can see that we wait for the search box to be available and then we add the searchTerm passed using page.evaluate. We then navigate to the products listing page by emulating the 'search button' click action and exposing the DOM.

The complexity of automation varies from use case to use case.

Some Notable Gotchas: A Minor Heads Up

Puppeteer's API is pretty comprehensive but there are a few gotchas I came across while working with it. Remember, not all of these gotchas are directly related to puppeteer but tend to work better along with it.

  • Puppeteer creates a Chrome browser instance as already mentioned. However, it is likely that some existing websites might block access if they suspect bot activity. There is this package called user-agents which can be used with puppeteer to randomize the user-agent for the browser.
Nota: Raspar un sitio web se encuentra en algún lugar de las áreas grises de aceptación legal. Recomendaría usarlo con precaución y verificar las reglas del lugar donde vive.
const puppeteer = require('puppeteer'); const userAgent = require('user-agents'); ... const browser = await puppeteer.launch({ headless: true, defaultViewport: null }); const page = await browser.newPage(); await page.setUserAgent(userAgent.toString()); ...
  • Nos encontramos defaultViewport: nullcuando lanzamos nuestra instancia de Chrome y la había incluido como opcional. Esto se debe a que es útil solo cuando está viendo la instancia de Chrome que se está iniciando. Evita que el ancho y la altura del sitio web se vean afectados cuando se procesa.
  • Puppeteer no es la solución definitiva cuando se trata de actuación. Usted, como desarrollador, tendrá que optimizarlo para aumentar su eficiencia de rendimiento a través de acciones como la limitación de animaciones en el sitio, permitiendo solo llamadas de red esenciales, etc.
  • Remember to always end a puppeteer session by closing the Browser instance by using browser.close. (I happened to miss out on it in the first try) It helps end a running Browser Session.
  • Certain common JavaScript operations like console.log() will not work within the scope of the page methods. The reason being that the page context/browser context differs from the node context in which your application is running.

These are some of the gotchas I noticed. If you have more, feel free to reach out to me with them. I would love to learn more.

Done? Let's run the application.

Website to Your API: Bringing it All Together

The application is run in non-headless mode so you can witness what exactly happens. We will automate the navigation to the product listing page from which we obtain the data.

There. You have your own API consumable data setup from the website of your choice. All you need to do now is to wire this up with a server side framework like express and you are good to go.

Conclusion

There is so much you can do with Puppeteer. This is just one particular use case. I would recommend that you spend some time to read the official documentation. I will be doing the same.

Puppeteer is used extensively in some of the largest organizations for automation tasks like testing and server side rendering, among others.

There is no better time to get started with Puppeteer than now.

If you have any questions or comments, you can reach out to me on LinkedIn or Twitter.

In the meantime, keep coding.