Comment créer une API personnalisée à partir de n'importe quel site Web à l'aide de Puppeteer

Il arrive souvent que vous tombiez sur un site Web et que vous soyez obligé d'effectuer un ensemble d'actions pour enfin obtenir des données. Vous êtes alors confronté à un dilemme: comment rendre ces données disponibles sous une forme facilement exploitable par votre application?

Le grattage vient à la rescousse dans un tel cas. Et choisir le bon outil pour le travail est très important.

Marionnettiste: pas seulement une autre bibliothèque de scraping

Puppeteer est une bibliothèque Node.js gérée par l'équipe Chrome Devtools de Google. Il exécute essentiellement une instance Chromium ou Chrome (peut-être le nom le plus reconnaissable) de manière sans tête (ou configurable) et expose un ensemble d'API de haut niveau.

D'après sa documentation officielle, le marionnettiste est normalement utilisé pour plusieurs processus qui ne se limitent pas aux suivants:

  • Générer des captures d'écran et des PDF
  • Exploration d'un SPA et génération de contenu pré-rendu (c.-à-d. Rendu côté serveur)
  • Tester les extensions Chrome
  • Test d'automatisation des interfaces Web
  • Diagnostic des problèmes de performances grâce à des techniques telles que la capture de la trace chronologique d'un site Web

Dans notre cas, nous devons pouvoir accéder à un site Web et cartographier les données sous une forme qui peut être facilement consommée par notre application.

Cela semble simple? La mise en œuvre n'est pas non plus si complexe. Commençons.

Enchaîner le code

Ma passion pour les produits Amazon m'incite à utiliser l'une de leurs pages de liste de produits comme exemple ici. Nous implémenterons notre cas d'utilisation en deux étapes:

  • Extraire les données de la page et les mapper sous une forme JSON facilement consommable
  • Ajoutez un peu d'automatisation pour nous rendre la vie un peu plus facile

Vous pouvez trouver le code complet dans ce référentiel.

Nous extrairons les données de ce lien: //www.amazon.in/s?k=Shirts&ref=nb_sb_noss_2 (une liste des chemises les plus recherchées comme indiqué dans l'image) sous une forme serviable par API.

Avant de commencer à utiliser puppeteer de manière approfondie dans cette section, nous devons comprendre les deux classes principales qu'il fournit.

  • Navigateur: lance une instance Chrome lorsque nous utilisons puppeteer.launchou puppeteer.connect. Cela fonctionne comme une simple émulation de navigateur.
  • Page: ressemble à un seul onglet sur un navigateur Chrome. Il fournit un ensemble exhaustif de méthodes que vous pouvez utiliser avec une instance de page particulière et est invoqué lorsque nous appelons browser.newPage. Tout comme vous pouvez créer plusieurs onglets dans le navigateur, vous pouvez de la même manière créer plusieurs instances de page à la fois dans marionnettiste.

Configuration de Puppeteer et navigation vers l'URL cible

Nous commençons à configurer marionnettiste en utilisant le module npm fourni. Après avoir installé marionnettiste, nous créons une instance du navigateur et de la classe de page et naviguons vers l'URL cible.

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); 

Nous utilisons networkidle2comme valeur pour l' waitUntiloption lors de la navigation vers l'URL. Cela garantit que l'état de chargement de la page est considéré comme final lorsqu'il n'y a pas plus de 2 connexions en cours d'exécution pendant au moins 500 ms.

Remarque: Vous n'avez pas besoin d'installer Chrome ou une instance de celui-ci sur votre système pour que le marionnettiste fonctionne. Il est déjà livré avec une version allégée fournie avec la bibliothèque.

Méthodes de page pour extraire et mapper des données

Le DOM est déjà chargé dans l'instance de page créée. Nous allons continuer et tirer parti de la page.evaluate()méthode pour interroger le DOM.

Avant de commencer, nous devons déterminer les points de données exacts que nous devons extraire. Dans l'exemple actuel, chacun des objets produit ressemblera à ceci.

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

Nous avons défini la structure que nous voulons réaliser. Il est temps de commencer à inspecter le DOM pour les identificateurs. Nous vérifions les sélecteurs qui se produisent dans les éléments à mapper. Nous utiliserons principalement document.querySelectoret document.querySelectorAllpour parcourir le 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; }); ... } ...

// parcours pour les noms de marques et de produits

Après avoir étudié le DOM, nous voyons que chaque élément répertorié est enfermé sous un élément avec le sélecteur div[data-cel-widget^="search_result_"]. Ce sélecteur particulier recherche toutes les divbalises avec l'attribut data-cel-widgetqui ont une valeur commençant par search_result_.

De même, nous mappons les sélecteurs pour les paramètres dont nous avons besoin, comme indiqué. Si vous souhaitez en savoir plus sur la traversée DOM, vous pouvez consulter cet article informatif de Zell.

  • total des éléments répertoriés:div[data-cel-widget^="search_result_"]
  • marque:div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base ( ireprésente le numéro de nœud dans total listed items)
  • produit:div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base  ou div[data-cel-widget="search_result_${i}"] .a-size-medium.a-color-base.a-text-normal( ireprésente le numéro de nœud dans total listed items)
  • url:div[data-cel-widget="search_result_${i}"] a[target="_blank"].a-link-normal ( ireprésente le numéro de nœud dans total listed items)
  • image:div[data-cel-widget="search_result_${i}"] .s-image ( ireprésente le numéro de nœud dans total listed items)
  • price:div[data-cel-widget="search_result_${i}"] span.a-offscreen ( ireprésente le numéro de nœud dans total listed items)
Remarque: Nous attendons que les div[data-cel-widget^="search_result_"]éléments nommés par sélecteur soient disponibles sur la page en utilisant la page.waitForméthode.

Une fois la page.evaluateméthode appelée, nous pouvons voir les données dont nous avons besoin enregistrées.

Adding Automation to Ease Flow

So far we are able to navigate to a page, extract the data we need, and transform it into an API-ready form. That sounds all hunky-dory.

However, consider for a moment a case where you have to navigate to one URL from another by performing some actions – and then try to extract the data you need.

Would that make your life a little trickier? Not at all. Puppeteer can easily imitate user behavior. Time to add some automation to our existing use case.

Unlike in the previous example, we will go to the amazon.in homepage and search for 'Shirts'. It will take us to the products listing page and we can extract the data required from the DOM. Easy peasy. Let's look at the code.

... 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.
Remarque: le raclage d'un site Web se situe quelque part dans les zones grises de l'acceptation légale. Je recommanderais de l'utiliser avec prudence et de vérifier les règles où vous vivez.
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()); ...
  • Nous sommes tombés sur defaultViewport: nulllors du lancement de notre instance Chrome et je l'avais répertoriée comme facultative. En effet, cela n'est utile que lorsque vous visualisez l'instance Chrome en cours de lancement. Cela évite que la largeur et la hauteur du site Web ne soient affectées lors de son rendu.
  • Puppeteer n'est pas la solution ultime en matière de performances. En tant que développeur, vous devrez l'optimiser pour augmenter son efficacité de performance grâce à des actions comme la limitation des animations sur le site, autoriser uniquement les appels réseau essentiels, 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.