Si vous utilisez fréquemment Puppeteer, il vous est peut-être déjà arrivé de vous demander comment exécuter plusieurs bots de façon simultanée. En effet, plutôt que de lancer vos scripts un par un, de façon successive, lorsque vous avez un nombre important de pages à scraper/automatiser, le meilleur moyen de gagner du temps est de pouvoir lancer des scripts en parallèle. Cependant, Pupeteer, dans son fonctionnement et son utilisation même, est un outil pensé pour un fonctionnement asynchrone (en gros, une tâche par une tâche). Produire un code permettant de paralléliser plusieurs bots Puppeteer risque d’être une tâche fastidieuse, car il va falloir contourner le comportement "initial" de cet outil.
Heureusement, des extensions ont été développées afin de répondre à ce besoin. Celle que je vais vous présenter aujourd’hui, et qui est probablement la plus utilisée ces derniers temps, s’appelle puppeteer-cluster. Développé par un français, cet outil est d’une qualité remarquable. Voyons plus en détail ce à quoi elle peut nous servir.
Comme dit précédemment, lorsque l’on a un travail de scraping ou d’automatisation à effectuer via Puppeteer, et que ce travail implique un nombre d’urls ou de tâches importantes, le besoin de gagner du temps se fait ressentir. Puppeteer-cluster, vous permet, comme son nom l’indique, de créer des cluster, c'est-à-dire des groupes de scripts qui vont s’exécuter de façon parallèle. Nous allon voir tout de suite ce que ça donne concrètement.
Tout d’abord, on installe le module via npm :
npm i –save puppeteer-cluster
On charge ensuite l’outil dans notre code, de la façon suivante :
const { Cluster } = require('puppeteer-cluster');
const cluster = await Cluster.launch({
concurrency: Cluster.CONCURRENCY_CONTEXT,
maxConcurrency: 3,
monitor: true,
});
Ici, on définit le comportement de notre cluster :
- Les paramètres "concurrency" et "maxConcurrency" nous permettent de régler le nombre de tâches en parallèles que sera capable de supporter notre cluster.
- Le paramètre "monitor" nous permet d’afficher, ou non, dans notre console, l’affichage en temps réel d’informations relatives à notre cluster (nom des scripts actuellement en exécution, RAM et ressources CPU utilisées, etc.).
Une fois le cluster configuré, comment mettre en œuvre le fonctionnement souhaité ? Admettons que nous devions scraper un site d’annonces immobilières. Nous disposons d’une liste d’URL à aller scraper, et nous souhaitons les intégrer à notre cluster, afin de pouvoir les scraper, non pas une par une, mais de façon simultanée. Pour ajouter une tâche au cluster, il faut utiliser la fonction suivante :
await cluster.queue(url);
Ensuite, pour donner les instructions qui vont devoir être appliquées à chaque élément inséré dans le cluster, il va falloir utiliser la fonction "task" :
await cluster.task(async ({ page, data: urls }) => {
await page.goto(urls);
// Vos instructions de scraping...
})
Afin de détecter si une erreur est venue interrompre un des scraping, il faut utiliser la fonction suivante :
cluster.on('taskerror', (err, data) => {
console.log(` Error crawling ${data}: ${err.message}`);
});
Au final, si l’on repart du cas selon lequel nous disposons d’une longue liste d’urls, et que nous souhaitons ajouter toute ces urls au cluster, voici à quoi va ressembler notre code :
const puppeteer = require('puppeteer');
const { Cluster } = require('puppeteer-cluster');
(async function() {
const cluster = await Cluster.launch({
concurrency: Cluster.CONCURRENCY_CONTEXT,
maxConcurrency: 3,
monitor: true,
});
const urls = [
"https://www.ogic.fr/provence-alpes-cote-d-azur/13/aix-en-provence/nouvelles-scenes/121",
"https://www.ogic.fr/provence-alpes-cote-d-azur/13/aix-en-provence/27-paul-cezanne/109",
"https://www.ogic.fr/auvergne-rhone-alpes/73/aix-les-bains/confidence-urbaine/209",
"https://www.ogic.fr/ile-de-france/94/alfortville/ar-home/113",
"https://www.ogic.fr/auvergne-rhone-alpes/74/annecy/triangle-d-or/107",
"https://www.ogic.fr/auvergne-rhone-alpes/74/annemasse/epure/145",
"https://www.ogic.fr/ile-de-france/95/argenteuil/variations/225",
"https://www.ogic.fr/ile-de-france/92/asnieres-sur-seine/panorama/59",
"https://www.ogic.fr/ile-de-france/92/asnieres-sur-seine/high-17/197",
"https://www.ogic.fr/ile-de-france/95/bezons/sequana/161",
"https://www.ogic.fr/ile-de-france/93/bobigny/south-canal/125",
"https://www.ogic.fr/ile-de-france/78/bois-d-arcy/jardins-gabin/199",
"https://www.ogic.fr/ile-de-france/92/boulogne-billancourt/mise-en-seine/103",
"https://www.ogic.fr/ile-de-france/92/bourg-la-reine/influence/216",
"https://www.ogic.fr/ile-de-france/77/bussy-saint-georges/le-clos-des-ormes/153",
"https://www.ogic.fr/ile-de-france/94/champigny-sur-marne/place-lenine/220",
"https://www.ogic.fr/auvergne-rhone-alpes/69/chassieu/jardins-divers/147",
"https://www.ogic.fr/ile-de-france/92/chatenay-malabry/coeur-de-ville/105",
"https://www.ogic.fr/ile-de-france/92/chaville/grand-place/177",
"https://www.ogic.fr/ile-de-france/92/chaville/carre-atrium/179",
"https://www.ogic.fr/ile-de-france/92/chaville/achrome/93",
"https://www.ogic.fr/ile-de-france/92/clichy/l-instant/219",
"https://www.ogic.fr/ile-de-france/77/combs-la-ville/helianthus/139",
"https://www.ogic.fr/ile-de-france/92/courbevoie/l-atelier/131",
"https://www.ogic.fr/auvergne-rhone-alpes/69/craponne/l-essentiel/157",
"https://www.ogic.fr/auvergne-rhone-alpes/01/divonne-les-bains/jardin-secret/43"
];
for (url of urls) {
await cluster.queue(url);
}
await cluster.task(async ({ page, data: urls }) => {
console.log(urls);
await page.goto(urls);
// Vos instructions de scraping...
})
// In case of problems, log them
cluster.on('taskerror', (err, data) => {
console.log(` Error crawling ${data}: ${err.message}`);
});
await cluster.idle();
await cluster.close();
})();
Lorsque vous allez exécuter un script comme celui-ci, le principe est simple : l’ensemble des urls va être inséré au cluster, et celles-ci vont être scrapées simultanément par groupe de 3 (compte tenu du fait que le paramètre maxConcurrency est ici fixé à 3). Les 3 premières vont être scrapées, une fois ces 3 urls traitées, on passe au groupe de 3 suivant, et ainsi de suite… Vous pouvez bien évidemment choisir un chiffre supérieur (ou inférieur) à 3, cependant, il va falloir faire attention aux ressources de la machine sur laquelle vous exécuter votre script. En effet, puppeteer est un outil assez énergivore, et le fait de constituer un cluster implique la création de plusieurs instances en parallèle. A titre d’exemple, lorsque j’exécute ce script sur mon ordinateur portable (Windows 10, i5-7300 @ 2,60GHz, 8Go de RAM) en local, au dela d’un paramètre maxConcurrency de 2 (donc au-delà de 2 tâches en parallèle par cluster), j’atteins les 100% d’utilisation des ressources CPU. En comparaison, je dispose cependant d’un VPS (Ubuntu, Intel(R) Xeon(R) E3-1270 v6 @ 3.80GHz, 32Go de RAM) sur lequel je peux monter jusqu’à un maxConcurrency de 16 sans problème de saturation.
Voila donc un exemple assez simple de ce qu'il est possible de faire grace à puppeteer-cluster. Comme vous le comprendrez assez rapidement, si vous êtes un habitué du web scraping ou des mécanismes d'automatisation, cette extension a de fortes chances de devenir un incontournable, vous permettant d'optimiser votre productivité et votre organisation de travail.