Titiritero no se comporta como en Developer Console

Sep 10 2020

Estoy tratando de extraer usando Puppeteer el título de esta página: https://www.nordstrom.com/s/zella-high-waist-studio-pocket-7-8-leggings/5460106

Tengo el siguiente código,

          (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 he probado usando la consola del navegador e incluso usando la opción {headless: false} de Puppeteer. Funciona como se esperaba en el navegador, pero cuando realmente lo ejecuto con el nodo, me da el siguiente error.

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

Entonces, cuando ejecuto la misma Array.from ...querySelectorAll("meta")...consulta en el navegador, obtengo la cadena esperada:

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

Empiezo a pensar que estoy haciendo algo mal con las promesas asíncronas, ya que esa es la parte que es diferente. ¿Alguien puede señalarme en la dirección correcta?

EDITAR: Como se sugirió, probé usando document.title, que debería estar allí, pero también devolvió nulo. Consulte el código y el registro a continuación:

          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 }

¿Tiene esto que ver con aplicaciones bs de una sola página? Pensé que el titiritero se encargaba de eso porque carga todo primero.

EDITAR: He agregado las líneas inactivas de red y espero 8000 milisegundos, como se sugiere. El título aún está vacío. Codifique a continuación y registre:

            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

EDITAR: PROGRESO !! Gracias a DavidBarton. Parece que sin cabeza tiene que ser falso para que funcione. ¿Alguien sabe por qué?

Respuestas

2 theDavidBarton Sep 10 2020 at 01:56

Si solo necesita el innerText de title, puede hacerlo con el page.$evalmétodo titiritero para lograr el mismo resultado:

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

Salida:

Zella High Waist Studio Pocket 7/8 Leggings | Nordstrom

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

El método page. $ Eval se ejecuta Array.from(document.querySelectorAll(selector))dentro de la página y lo pasa como primer argumento a pageFunction.


Sin embargo: su principal problema es que la página que está visitando es una aplicación de página única (SPA) creada en React.Js, y titlese llena dinámicamente con el paquete de JavaScript. Entonces, su titiritero encuentra un titleelemento válido en <head>cuando su contenido es simplemente: ""(una cadena vacía).

Normalmente, debe usar waitUntil: 'networkidle0'en el caso de SPA para asegurarse de que el DOM esté poblado por el marco JS real correctamente y sea completamente funcional:

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

Desafortunadamente, con este sitio web específico arroja un error de tiempo de espera ya que las conexiones de red no se cierran hasta el tiempo de espera predeterminado de 30000 ms, algo parece no estar bien en el lado de la interfaz de la página web (¿manejo del trabajador web?).

Como solución alternativa, puede obligar al titiritero a dormir durante 8 segundos con: await page.waitFor(8000)antes de intentar recuperar el title: en ese momento estará correctamente poblado. En realidad, cuando ejecuta su script en DevTools Console, funciona porque no está ejecutando inmediatamente el script: esa vez que la página ya está completamente cargada, DOM se completa.

Este script devolverá el título esperado:

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

Quizás también const browser = await puppeteer.launch({ headless: false })afecte el resultado.

1 chuklore Sep 10 2020 at 01:18

al navegar a la página, espere hasta que se cargue la página

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

¿Podrías intentar esto?

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

}

o esto

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

   }