2 votes

Puppeteer ne se comporte pas comme dans la Console du développeur

J'essaie d'extraire le titre de cette page en utilisant Puppeteer : https://www.nordstrom.com/s/zella-high-waist-studio-pocket-7-8-leggings/5460106

J'ai le code suivant,

          (async () => {
            const browser = await puppet.launch({ headless: true });
            const page = await browser.newPage();
            await page.goto(req.params[0]); //this is the url
            title = await page.evaluate(() => {
              Array.from(document.querySelectorAll("meta")).filter(function (
                el
              ) {
                return (
                  (el.attributes.name !== null &&
                    el.attributes.name !== undefined &&
                    el.attributes.name.value.endsWith("title")) ||
                  (el.attributes.property !== null &&
                    el.attributes.property !== undefined &&
                    el.attributes.property.value.endsWith("title"))
                );
              })[0].attributes.content.value ||
                document.querySelector("title").innerText;
            });

que j'ai testé en utilisant la console du navigateur et même en utilisant l'option {headless : false } de Puppeteer. Il fonctionne comme prévu dans le navigateur, mais lorsque je l'exécute avec node, il me donne l'erreur suivante.

10:54:21 AM web.1 |  (node:10288) UnhandledPromiseRejectionWarning: Error: Evaluation failed: TypeError: Cannot read property 'attributes' of undefined
10:54:21 AM web.1 |      at __puppeteer_evaluation_script__:14:20

Donc, quand je lance le même Array.from ...querySelectorAll("meta")... dans le navigateur, j'obtiens la chaîne attendue :

"Zella High Waist Studio Pocket 7/8 Leggings | Nordstrom"

Je commence à penser que je fais quelque chose de mal avec les promesses asynchrones, car c'est la partie qui est différente. Quelqu'un peut-il m'indiquer la bonne direction ?

EDIT : Comme suggéré, j'ai testé en utilisant document.title, qui devrait être là, mais il a également retourné null. Voir le code et le journal ci-dessous :

          console.log(
            "testing the return",
            (async () => {
              const browser = await puppet.launch({ headless: true });
              const page = await browser.newPage();
              await page.goto(req.params[0]); //this is the url
              try {
                title = await page.evaluate(() => {
                  const title = document.title;
                  const isTitleThere = title == null ? false : true;
                  //recently read that this checks for undefined as well as null but not an
                  //undeclared var
                  return {
                    title: title,
                    titleTitle: title.title,
                    isTitleThere: isTitleThere,
                  };
                });
              } catch (error) {
                console.log(error, "There was an error");
              }

11:54:11 AM web.1 |  testing the return Promise { <pending> }
11:54:13 AM web.1 |  { title: '', isTitleThere: true }

Est-ce que ça a un rapport avec la page unique de l'application Bs ? Je pensais que Puppeteer gérait ça parce qu'il charge tout en premier.

EDIT : J'ai ajouté les lignes networkidle et j'attends 8000 millisecondes, comme suggéré. Le titre est toujours vide. Code ci-dessous et log :

            await page.goto(req.params[0], { waitUntil: "networkidle2" });
            await page.waitFor(8000);
            console.log("done waiting");
            title = await page.$eval("title", (el) => el.innerText);
            console.log("title: ", title);
            console.log("done retrieving");

12:36:39 PM web.1 |  done waiting
12:36:39 PM web.1 |  title:  
12:36:39 PM web.1 |  done retreiving

EDIT : PROGRÈS !! Merci à theDavidBarton. Il semble que headless doive être false pour que cela fonctionne ? Quelqu'un sait-il pourquoi ?

1voto

chuklore Points 173

Lorsque vous naviguez vers la page, attendez que la page soit chargée.

await page.goto(req.params[0], { waitUntil: "networkidle2" }); //this is the url

Pourriez-vous essayer ceci

 try {
    title = await page.evaluate(() => {
        const title = document.title;
        const isTitleThere = title == null? false: true
        //recently read that this checks for undefined as well as null but not an 
        //undeclared var
        return {"title":title,"isTitleThere" :isTitleThere }
    })

} catch (error) {
    console.log(error, 'There was an error');

}

ou ceci

 try {
title = await page.evaluate(() => {
    const title = document.querySelector('meta[property="og:title"]');
    const isTitleThere = title == null? false: true
    //recently read that this checks for undefined as well as null but not an 
    //undeclared var
    return {"title":title,"isTitleThere" :isTitleThere }
   })

   } catch (error) {
   console.log(error, 'There was an error');

   }

1voto

theDavidBarton Points 3419

Si vous n'avez besoin que de l'innerText de title vous pourriez le faire avec page.$eval méthode du marionnettiste pour obtenir le même résultat :

const title = await page.$eval('title', el => el.innerText)
console.log(title)

Sortie :

Zella High Waist Studio Pocket 7/8 Leggings | Nordstrom

page.$$eval(selector, pageFunction[, ...args])

La méthode page.$eval exécute Array.from(document.querySelectorAll(selector)) dans la page et le passe comme premier argument à pageFunction.


Cependant, le problème principal est que la page que vous visitez est une SPA (Single-Page App) réalisée en React.Js, et que sa title est remplie dynamiquement par le bundle JavaScript. Ainsi, votre marionnettiste trouve un title dans l'élément <head> lorsque son contenu est simplement : "" (une chaîne vide).

Normalement, vous devriez utiliser waitUntil: 'networkidle0' dans le cas des SPA, pour s'assurer que le DOM est correctement rempli par le cadre JS actuel et qu'il est pleinement fonctionnel :

await page.goto('https://www.nordstrom.com/s/zella-high-waist-studio-pocket-7-8-leggings/5460106', {
    waitUntil: 'networkidle0'
  })

Malheureusement, avec ce site Web spécifique, une erreur de délai d'attente se produit, car les connexions réseau ne se ferment pas avant le délai d'attente par défaut de 30000 ms. Il semble que quelque chose ne soit pas correct du côté du frontal de la page Web (gestion du webworker ?).

Comme solution de contournement, vous pouvez forcer le marionnettiste à dormir pendant 8 secondes avec : await page.waitFor(8000) avant d'essayer de récupérer le title : à ce moment-là, il sera correctement peuplé. En fait, lorsque vous exécutez votre script dans DevTools Console, cela fonctionne parce que vous n'exécutez pas immédiatement le script : à ce moment-là, la page est déjà entièrement chargée, le DOM est peuplé.

Ce script renverra le titre attendu :

async function fn() {
  const browser = await puppeteer.launch({ headless: false })
  const page = await browser.newPage()

  await page.goto('https://www.nordstrom.com/s/zella-high-waist-studio-pocket-7-8-leggings/5460106', {
    waitUntil: 'networkidle2'
  })
  await page.waitFor(8000)

  const title = await page.$eval('title', el => el.innerText)
  console.log(title)

  await browser.close()
}
fn()

Peut-être const browser = await puppeteer.launch({ headless: false }) affecte également le résultat.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X