Puppeteer , délai entre chaque page.goto(url)

Bonjour,
j’apprends le scrapping avec le tuto suivant, dans Exemple #3

J’aimerai l’améliorer en mettant un délai aléatoire entre chaque ouverture d’URL, se qui permettra de rendre le scrap plus naturel.

Dans la fonction getDataFromUrl j’ai mis « page.waitFor(5000) » avant « await page.goto(url) », mais c’est seulement pris en compte lors de l’accès à la 1ère URL.

Vous pourriez me guider SVP ?
Merci.

Bien le bonjour :slight_smile:

Il faut bien comprendre la nature inhérente à JavaScript, c’est à dire: sa nature asynchrone.

En d’autres termes: on ne peut malheureusement pas juste écrire des lignes d’instructions les unes en dessous des autres en pensant que celles-ci vont être exécutées de manière séquentielle.

En effet, certaines opérations sont asynchrones, et nécessitent que l’on force notre programme à attendre qu’elles soient terminées, avant de pouvoir passer à la ligne suivante: d’où l’utilisation du mot clé await ici :slight_smile: .

await bloque l’exécution du code tant que la promesse n’est pas terminée (source ici)

Sinon, notre programme exécute la ligne d’après, sans se soucier que la ligne en cours soit terminée :confused:

Pour faire hyper simple, sans trop rentrer dans les détails: à chaque fois que tu veux appeler une fonction, et que la documentation indique que cette dernière renvoie une <Promise> , alors il faut systématiquement précéder l’appel à cette fonction par le mot clé await.

Or, d’après la documentation de l’API de Puppeteer:

Donc pour que ça fonctionne comme attendu, il te faut:

await page.waitFor(5000);
await page.goto(url);

Et pour le coté random, tu peux faire cela:

const getRandomFloat = (min, max) => Math.random() * (max - min) + min;

// Attend un nombre aléatoire de secondes, entre 1 et 5s
await page.waitFor(getRandomFloat(1,5));
await page.goto(url);
1 J'aime

Merci pour les explications :wink:
Par contre j’ai bien mis « await page.waitFor(5000) »; et cette pause se fait juste pour l’affichage de la 1ère URL, les autres url s’ouvrent d’un coup.

voici ma fonction getDataFromUrl :

const getDataFromUrl = async (browser, url) => {
  const page = await browser.newPage()
  await page.waitFor(5000)
  await page.goto(url)
  await page.waitFor('body')
  return page.evaluate(() => {
let title = document.querySelector('h1').innerText
let price = document.querySelector('.price_color').innerText
return { title, price }
  })
}

Il manque le reste de ton code, en particulier les lignes où tu appelles ta fonction getDataFromURL.

Peux-tu partager l’ensemble?

Voicl le code complet, c’est le même que le tuto :

const puppeteer = require('puppeteer')


const getAllUrl = async browser => {
  const page = await browser.newPage()
  await page.goto('http://books.toscrape.com/')
  await page.waitFor('body')
  const result = await page.evaluate(() =>
    [...document.querySelectorAll('.product_pod a')].map(link => link.href),

  )
  return result
}


const getDataFromUrl = async (browser, url) => {
  const page = await browser.newPage()
  await page.waitFor(5000)
  await page.goto(url)
  await page.waitFor('body')
  return page.evaluate(() => {
    let title = document.querySelector('h1').innerText
    let price = document.querySelector('.price_color').innerText
    return { title, price }
  })
}


const scrap = async () => {
  const browser = await puppeteer.launch({ headless: false })
  const urlList = await getAllUrl(browser) 
  const results = await Promise.all(
    urlList.map(url => getDataFromUrl(browser, url)),
  )
  browser.close()
  return results
}


scrap()
  .then(value => {
    console.log(value)
  })
  .catch(e => console.log(`error: ${e}`))

Quel est le problème en fait?

Car le script fonctionne exactement suivant le comportement désiré, c’est à dire ouvrir toutes les pages en même temps (en parallèle) :slight_smile:

Est-ce que ce que tu essayes de dire c’est que, tu ne veux pas de ce comportement par défaut, mais plutôt ouvrir les pages les unes à la suite des autres (en séquentiel, pas en parallèle) ?

Oui c’est ça, en séquentiel, pas en parallèle, j’aimerai que le scrap soit plus naturel comme si c’était un vrai visiteur et donc mettre une limite entre chaque ouverture d’url.

Ok, dans ce cas, modifie le code pour que les lignes 54 et 55 (l’appel à Promise.all avec urlList.map) soient remplacées par une boucle for(let … of), tel que:

// 1 - Import de puppeteer
const puppeteer = require('puppeteer');

const getRandomFloat = (min, max) => Math.random() * (max - min) + min;


/*
// 2 - Récupération des URLs de toutes les pages à visiter
- waitFor("body"): met le script en pause le temps que la page se charge
- document.querySelectorAll(selector): renvoie tous les noeuds qui vérifient le selecteur
- [...document.querySelectorAll(selector)]: caste les réponses en tableau
- Array.map(link => link.href): récupère les attributs href de tous les liens
*/
const getAllUrl = async browser => {
  const page = await browser.newPage();
  await page.goto('http://books.toscrape.com/');
  await page.waitFor('body');
  const result = await page.evaluate(() =>
    [...document.querySelectorAll('.product_pod a')].map(link => link.href),
  );
  await page.close();
  return result;
}

// 3 - Récupération du prix et du tarif d'un livre à partir d'une url (voir exo #2)
const getDataFromUrl = async (browser, url) => {
  const page = await browser.newPage();
  await page.waitFor(getRandomFloat(1000,3000));
  await page.goto(url);
  await page.waitFor('body');
  const result = await page.evaluate(() => {
    let title = document.querySelector('h1').innerText;
    let price = document.querySelector('.price_color').innerText;
    return { title, price };
  });
  await page.close();
  return result;
}

/*
// 4 - Fonction principale : instanciation d'un navigateur et renvoi des résultats
- urlList.map(url => getDataFromUrl(browser, url)):
appelle la fonction getDataFromUrl sur chaque URL de `urlList` et renvoi un tableau

- await Promise.all(promesse1, promesse2, promesse3):
bloque de programme tant que toutes les promesses ne sont pas résolues
*/
const scrap = async () => {
  const results = [];
  const browser = await puppeteer.launch({ headless: false });
  const urlList = await getAllUrl(browser);

  /*
  // Ici on ouvre toutes les pages en parallèle et on attend que toutes
  // les promises renvoyées soient résolues avant de passer à la suite
  const results = await Promise.all(
    urlList.map(url => getDataFromUrl(browser, url)),
  ); */

  // Ici, on itère sur la liste des URLs dont il faut traiter les pages
  // et on bloque le programme tant que la page en cours n'est pas terminée
  // de telle sorte à rendre l'ouverture de chaque page séquentielle
  for(let url of urlList) {
      const result = await getDataFromUrl(browser, url);
      results.push(result);
  }

  await browser.close();
  return results;
}

// 5 - Appel la fonction `scrap()`, affichage les résulats et catch les erreurs
scrap()
  .then(value => {
    console.log(value);
  })
  .catch(e => console.log(`error: ${e}`));
1 J'aime

Merci beaucoup l’ouverture des URl est bien selon le random :grinning:

Par contre je viens de me rendre compte que le tuto a un Bug, getAllUrl génère 2 fois chaque URL. Et donc chaque URL est ouvert 2 fois.

Je viens de voir que dans la source, il y a en effet 2 href dans la même DIV.
document.querySelectorAll(’.product_pod a’) n’est pas sensé prendre que le 1er enfant href ?

Pour régler je cible mieux le noeud en question :
document.querySelectorAll(’.image_container a’)

Non car justement le but de document.querySelectorAll c’est de renvoyer la liste de toutes les nodes qui matchent le sélecteur passé en paramètre.

Si deux nodes enfants sont matchées par le même sélecteurs, elles seront retournées.

Si jamais un meilleur ciblage par sélecteur ne suffisait pas, tu peux toujours dédoublonner les values de ton array via un nouveau concept introduit avec ES6, j’ai nommé les « Set » avec spread operator, tel que:

const getAllUrl = async browser => {
  const page = await browser.newPage();
  await page.goto('http://books.toscrape.com/');
  await page.waitFor('body');
  const result = await page.evaluate(() =>
    [...document.querySelectorAll('.product_pod a')].map(link => link.href),
  );
  await page.close();
  return [ ...new Set(result) ];
}

Merci j’en prends note.