Titiritero no se comporta como en Developer Console
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
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.
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');
}