Web Scraping avec un navigateur sans tête : un tutoriel de marionnettiste
Publié: 2022-03-11Dans cet article, nous verrons à quel point il est facile d'effectuer du scraping Web (automatisation Web) avec la méthode quelque peu non traditionnelle d'utilisation d'un navigateur sans tête .
Qu'est-ce qu'un navigateur sans tête et pourquoi est-il nécessaire ?
Au cours des dernières années, le Web a évolué à partir de sites Web simplistes construits avec du HTML et du CSS nus. Il existe maintenant beaucoup plus d'applications Web interactives avec de belles interfaces utilisateur, qui sont souvent construites avec des frameworks tels que Angular ou React. En d'autres termes, de nos jours, JavaScript régit le Web, y compris presque tout ce avec quoi vous interagissez sur les sites Web.
Pour nos besoins, JavaScript est un langage côté client. Le serveur renvoie des fichiers ou des scripts JavaScript injectés dans une réponse HTML et le navigateur les traite. Maintenant, c'est un problème si nous faisons une sorte de grattage Web ou d'automatisation Web car le plus souvent, le contenu que nous aimerions voir ou racler est en fait rendu par du code JavaScript et n'est pas accessible à partir de la réponse HTML brute que le serveur délivre.
Comme nous l'avons mentionné ci-dessus, les navigateurs savent comment traiter le JavaScript et afficher de belles pages Web. Et si nous pouvions tirer parti de cette fonctionnalité pour nos besoins de scraping et avoir un moyen de contrôler les navigateurs par programmation ? C'est exactement là qu'intervient l'automatisation du navigateur sans tête !
Sans tête? Excuse-moi? Oui, cela signifie simplement qu'il n'y a pas d'interface utilisateur graphique (GUI). Au lieu d'interagir avec les éléments visuels comme vous le feriez normalement, par exemple avec une souris ou un appareil tactile, vous automatisez les cas d'utilisation avec une interface de ligne de commande (CLI).
Chrome sans tête et marionnettiste
Il existe de nombreux outils de grattage Web qui peuvent être utilisés pour la navigation sans tête, comme Zombie.js ou Firefox sans tête utilisant Selenium. Mais aujourd'hui, nous allons explorer Chrome sans tête via Puppeteer, car il s'agit d'un lecteur relativement récent, sorti début 2018. Note de l'éditeur : il convient de mentionner le navigateur distant d'Intoli, un autre nouveau lecteur, mais cela devra être un sujet pour un autre article.
Qu'est-ce que Marionnettiste ? Il s'agit d'une bibliothèque Node.js qui fournit une API de haut niveau pour contrôler Chrome ou Chromium sans tête ou pour interagir avec le protocole DevTools. Il est maintenu par l'équipe Chrome DevTools et une formidable communauté open source.
Assez parlé, plongeons dans le code et explorons le monde de la façon d'automatiser le web scraping à l'aide de la navigation sans tête de Puppeteer !
Préparation de l'environnement
Tout d'abord, vous devez avoir Node.js 8+ installé sur votre machine. Vous pouvez l'installer ici, ou si vous êtes un amateur de CLI comme moi et que vous aimez travailler sur Ubuntu, suivez ces commandes :
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - sudo apt-get install -y nodejs
Vous aurez également besoin de certains packages qui peuvent ou non être disponibles sur votre système. Juste pour être sûr, essayez d'installer ceux-ci:
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
Configurer Headless Chrome et Marionnettiste
Je recommanderais d'installer Puppeteer avec npm
, car il inclura également la version stable à jour de Chromium qui est garantie de fonctionner avec la bibliothèque.
Exécutez cette commande dans le répertoire racine de votre projet :
npm i puppeteer --save
Remarque : Cela peut prendre un certain temps, car Puppeteer devra télécharger et installer Chromium en arrière-plan.
Bon, maintenant que nous sommes tous prêts et configurés, que le plaisir commence !
Utilisation de l'API Puppeteer pour le scraping Web automatisé
Commençons notre didacticiel Puppeteer avec un exemple de base. Nous allons écrire un script qui amènera notre navigateur sans tête à prendre une capture d'écran d'un site Web de notre choix.
Créez un nouveau fichier dans votre répertoire de projet nommé screenshot.js
et ouvrez-le dans votre éditeur de code préféré.
Tout d'abord, importons la bibliothèque Puppeteer dans votre script :
const puppeteer = require('puppeteer');
Ensuite, prenons l'URL des arguments de ligne de commande :
const url = process.argv[2]; if (!url) { throw "Please provide a URL as the first argument"; }
Maintenant, nous devons garder à l'esprit que Puppeteer est une bibliothèque basée sur des promesses : elle effectue des appels asynchrones vers l'instance Chrome sans tête sous le capot. Gardons le code propre en utilisant async/wait. Pour cela, nous devons d'abord définir une fonction async
et y mettre tout le code Puppeteer :
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();
Au total, le code final ressemble à ceci :
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();
Vous pouvez l'exécuter en exécutant la commande suivante dans le répertoire racine de votre projet :
node screenshot.js https://github.com
Attendez une seconde, et boum ! Notre navigateur sans tête vient de créer un fichier nommé screenshot.png
et vous pouvez y voir la page d'accueil GitHub. Génial, nous avons un grattoir Web Chrome qui fonctionne !
Arrêtons-nous une minute et explorons ce qui se passe dans notre run()
ci-dessus.
Tout d'abord, nous lançons une nouvelle instance de navigateur sans tête, puis nous ouvrons une nouvelle page (onglet) et naviguons vers l'URL fournie dans l'argument de ligne de commande. Enfin, nous utilisons la méthode intégrée de Puppeteer pour prendre une capture d'écran, et nous n'avons qu'à fournir le chemin où elle doit être enregistrée. Nous devons également nous assurer de fermer le navigateur sans tête après avoir terminé notre automatisation.
Maintenant que nous avons couvert les bases, passons à quelque chose d'un peu plus complexe.
Un deuxième exemple de grattage de marionnettiste
Pour la prochaine partie de notre didacticiel Puppeteer, disons que nous voulons récupérer les articles les plus récents de Hacker News.
Créez un nouveau fichier nommé ycombinator-scraper.js
et collez-y l'extrait de code suivant :
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);
D'accord, il se passe un peu plus ici par rapport à l'exemple précédent.
La première chose que vous remarquerez peut-être est que la run()
renvoie maintenant une promesse, de sorte que le préfixe async
a été déplacé vers la définition de la fonction promise.
Nous avons également enveloppé tout notre code dans un bloc try-catch afin de pouvoir gérer toutes les erreurs qui entraînent le rejet de notre promesse.
Et enfin, nous utilisons la méthode intégrée de Puppeteer appelée evaluate()
. Cette méthode nous permet d'exécuter du code JavaScript personnalisé comme si nous l'exécutions dans la console DevTools. Tout ce qui est renvoyé par cette fonction est résolu par la promesse. Cette méthode est très pratique lorsqu'il s'agit de récupérer des informations ou d'effectuer des actions personnalisées.
Le code passé à la méthode evaluate()
est un JavaScript assez basique qui construit un tableau d'objets, chacun ayant des champs d' url
et de text
qui représentent les URL de l'histoire que nous voyons sur https://news.ycombinator.com/.
La sortie du script ressemble à ceci (mais avec 30 entrées, à l'origine) :
[ { 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' } ]
Plutôt chouette, je dirais !
Bon, avançons. Nous n'avons eu que 30 articles retournés, alors qu'il y en a beaucoup d'autres disponibles, ils ne sont que sur d'autres pages. Nous devons cliquer sur le bouton "Plus" pour charger la page suivante de résultats.

Modifions un peu notre script pour ajouter un support pour la pagination :
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);
Passons en revue ce que nous avons fait ici :
- Nous avons ajouté un seul argument appelé
pagesToScrape
à notrerun()
principale. Nous allons l'utiliser pour limiter le nombre de pages que notre script va récupérer. - Il existe une autre nouvelle variable nommée
currentPage
qui représente le numéro de la page de résultats que nous examinons actuellement. Il est initialement défini sur1
. Nous avons également enveloppé notre fonctionevaluate()
dans une bouclewhile
, de sorte qu'elle continue de s'exécuter tant quecurrentPage
est inférieur ou égal àpagesToScrape
. - Nous avons ajouté le bloc pour passer à une nouvelle page et attendre que la page se charge avant de redémarrer la boucle
while
.
Vous remarquerez que nous avons utilisé la méthode page.click()
pour que le navigateur sans tête clique sur le bouton "Plus". Nous avons également utilisé la méthode waitForSelector()
pour nous assurer que notre logique est en pause jusqu'à ce que le contenu de la page soit chargé.
Ces deux méthodes sont des méthodes d'API Puppeteer de haut niveau prêtes à l'emploi.
L'un des problèmes que vous rencontrerez probablement lors du grattage avec Puppeteer est l'attente du chargement d'une page. Hacker News a une structure relativement simple et il était assez facile d'attendre la fin du chargement de sa page. Pour les cas d'utilisation plus complexes, Puppeteer propose une large gamme de fonctionnalités intégrées, que vous pouvez explorer dans la documentation de l'API sur GitHub.
Tout cela est plutôt cool, mais notre tutoriel Puppeteer n'a pas encore couvert l'optimisation. Voyons comment faire fonctionner Puppeteer plus rapidement.
Optimiser notre script de marionnettiste
L'idée générale est de ne pas laisser le navigateur sans tête faire de travail supplémentaire. Cela peut inclure le chargement d'images, l'application de règles CSS, le déclenchement de requêtes XHR, etc.
Comme avec d'autres outils, l'optimisation de Puppeteer dépend du cas d'utilisation exact, alors gardez à l'esprit que certaines de ces idées pourraient ne pas convenir à votre projet. Par exemple, si nous avions évité de charger des images dans notre premier exemple, notre capture d'écran n'aurait peut-être pas ressemblé à ce que nous voulions.
Quoi qu'il en soit, ces optimisations peuvent être réalisées soit en mettant en cache les actifs à la première requête, soit en annulant purement et simplement les requêtes HTTP lorsqu'elles sont initiées par le site Web.
Voyons d'abord comment fonctionne la mise en cache.
Vous devez savoir que lorsque vous lancez une nouvelle instance de navigateur sans tête, Puppeteer crée un répertoire temporaire pour son profil. Il est supprimé lorsque le navigateur est fermé et n'est pas disponible lorsque vous lancez une nouvelle instance. Ainsi, toutes les images, CSS, cookies et autres objets stockés ne seront plus accessibles.
Nous pouvons forcer Puppeteer à utiliser un chemin personnalisé pour stocker des données telles que les cookies et le cache, qui seront réutilisés chaque fois que nous l'exécuterons à nouveau, jusqu'à ce qu'ils expirent ou soient supprimés manuellement.
const browser = await puppeteer.launch({ userDataDir: './data', });
Cela devrait nous donner une belle augmentation des performances, car de nombreux CSS et images seront mis en cache dans le répertoire de données lors de la première demande, et Chrome n'aura pas besoin de les télécharger encore et encore.
Cependant, ces actifs seront toujours utilisés lors du rendu de la page. Dans nos besoins de grattage des articles de presse Y Combinator, nous n'avons pas vraiment besoin de nous soucier des visuels, y compris les images. Nous ne nous soucions que de la sortie HTML nue, essayons donc de bloquer chaque requête.
Heureusement, Puppeteer est plutôt cool de travailler avec, dans ce cas, car il prend en charge les crochets personnalisés. Nous pouvons fournir un intercepteur à chaque demande et annuler celles dont nous n'avons pas vraiment besoin.
L'intercepteur peut être défini de la manière suivante :
await page.setRequestInterception(true); page.on('request', (request) => { if (request.resourceType() === 'document') { request.continue(); } else { request.abort(); } });
Comme vous pouvez le voir, nous avons un contrôle total sur les demandes qui sont initiées. Nous pouvons écrire une logique personnalisée pour autoriser ou abandonner des requêtes spécifiques en fonction de leur resourceType
. Nous avons également accès à de nombreuses autres données telles que request.url
afin que nous puissions bloquer uniquement des URL spécifiques si nous le souhaitons.
Dans l'exemple ci-dessus, nous n'autorisons que les requêtes avec le type de ressource "document"
à traverser notre filtre, ce qui signifie que nous bloquerons toutes les images, CSS et tout le reste en plus de la réponse HTML d'origine.
Voici notre code 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);
Restez en sécurité avec les limites de débit
Les navigateurs sans tête sont des outils très puissants. Ils sont capables d'effectuer presque n'importe quel type de tâche d'automatisation Web, et Puppeteer rend cela encore plus facile. Malgré toutes les possibilités, nous devons respecter les conditions d'utilisation d'un site Web pour nous assurer de ne pas abuser du système.
Étant donné que cet aspect est davantage lié à l'architecture, je ne le couvrirai pas en profondeur dans ce didacticiel Puppeteer. Cela dit, la manière la plus basique de ralentir un script Puppeteer est de lui ajouter une commande sleep :
js await page.waitFor(5000);
Cette instruction forcera votre script à se mettre en veille pendant cinq secondes (5000 ms). Vous pouvez le mettre n'importe où avant browser.close()
.
Tout comme limiter votre utilisation de services tiers, il existe de nombreuses autres façons plus robustes de contrôler votre utilisation de Puppeteer. Un exemple serait la construction d'un système de file d'attente avec un nombre limité de travailleurs. Chaque fois que vous souhaitez utiliser Puppeteer, vous poussez une nouvelle tâche dans la file d'attente, mais il n'y a qu'un nombre limité de travailleurs capables de travailler sur les tâches qu'elle contient. Il s'agit d'une pratique assez courante lorsqu'il s'agit de limites de débit d'API tierces et peut également être appliquée au grattage de données Web Puppeteer.
La place du marionnettiste dans le Web en mouvement rapide
Dans ce didacticiel Puppeteer, j'ai démontré sa fonctionnalité de base en tant qu'outil de grattage Web. Cependant, il a des cas d'utilisation beaucoup plus larges, y compris les tests de navigateur sans tête, la génération de PDF et la surveillance des performances, entre autres.
Les technologies Web évoluent rapidement. Certains sites Web dépendent tellement du rendu JavaScript qu'il est devenu presque impossible d'exécuter de simples requêtes HTTP pour les récupérer ou effectuer une sorte d'automatisation. Heureusement, les navigateurs sans tête deviennent de plus en plus accessibles pour gérer tous nos besoins d'automatisation, grâce à des projets comme Puppeteer et les équipes géniales derrière eux !