【问题标题】:Puppeteer not behaving like in Developer ConsolePuppeteer 在开发者控制台中的行为不像
【发布时间】:2020-12-28 05:04:23
【问题描述】:

我正在尝试使用 Puppeteer 提取此页面的标题:https://www.nordstrom.com/s/zella-high-waist-studio-pocket-7-8-leggings/5460106

我有以下代码,

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

我已经使用浏览器控制台进行了测试,甚至使用了 Puppeteer 的 { headless: false } 选项。它在浏览器中按预期工作,但是当我实际使用节点运行它时,它给了我以下错误。

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

所以,当我在浏览器中运行相同的 Array.from ...querySelectorAll("meta")... 查询时,我得到了预期的字符串:

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

我开始认为我对异步承诺做错了,因为那是不同的部分。谁能指出我正确的方向?

编辑:按照建议,我使用 document.title 进行了测试,它应该在那里,但它也返回 null。请参阅下面的代码和日志:

          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 }

这与单页应用程序bs有关吗?我认为 puppeteer 处理了这个问题,因为它首先加载所有内容。

编辑:按照建议,我添加了 networkidle 行并等待 8000 毫秒。标题仍然是空的。下面的代码和日志:

            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

编辑:进展!! 感谢大卫巴顿。似乎无头必须是假的才能起作用?有谁知道为什么?

【问题讨论】:

    标签: javascript node.js web-scraping puppeteer


    【解决方案1】:

    当导航到页面时,等待页面加载

    await page.goto(req.params[0], { waitUntil: "networkidle2" }); //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,"isTitleThere" :isTitleThere }
        })
    
    } catch (error) {
        console.log(error, 'There was an error');
    
    }
    

    或者这个

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

    【讨论】:

    • 我尝试了第一个。它返回 true :( 但我正在查看的页面中肯定有一个文档标题。
    • 你可以像这样访问标题title.title
    • 我不是。我可以做? :0 我只希望这一个函数是异步的。它可以在等待时完成其余的工作,这是我的想法。这是错的吗?我应该将我的整个代码包装在一个异步函数中吗?
    • 为什么是 networkidle2 而不是 networkidle0 或 1?
    • 当我遇到这个问题时,我使用了这个 url Puppeteer wait until page is completely loaded - Stack Overflow 的解决方案
    【解决方案2】:

    如果您只需要title 的innerText,您可以使用page.$eval puppeteer 方法来实现相同的结果:

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

    输出:

    Zella High Waist Studio Pocket 7/8 Leggings | Nordstrom
    

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

    page.$eval 方法在页面内运行 Array.from(document.querySelectorAll(selector)) 并将其作为第一个参数传递给 pageFunction。


    但是:您的主要问题是您正在访问的页面是用 React.Js 制作的单页应用程序 (SPA),它的 title 由 JavaScript 包动态填充。所以你的 puppeteer 在 &lt;head&gt; 中找到一个有效的 title 元素,而它的内容只是:""(一个空字符串)。

    通常您应该在 SPA 的情况下使用 waitUntil: 'networkidle0' 以确保 DOM 由实际的 JS 框架正确填充并且功能齐全:

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

    不幸的是,对于这个特定的网站,它会引发超时错误,因为网络连接在 30000 毫秒默认超时之前不会关闭,网页前端似乎有些问题(网络工作者处理?)。

    作为一种解决方法,您可以在尝试检索 title 之前强制 puppeteer 休眠 8 秒:await page.waitFor(8000)到那时它将正确填充。实际上,当您在 DevTools 控制台中运行脚本时,它可以工作,因为您没有立即运行脚本:页面已经完全加载时,DOM 已填充。

    此脚本将返回预期的标题:

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

    也许const browser = await puppeteer.launch({ headless: false }) 也会影响结果。

    【讨论】:

    • 即使使用 networkidle 和 8000,它仍然返回空。即使在等待之后,它是否可能没有完全加载?还是我做错了什么?
    • 如何使用networkidle?如果您使用 networkidle0 您的整个脚本可能会失败。我的脚本只有这 3 行(在 page.goto 之后),它当前返回了标题。
    • 我试过networkidle2和networkidle0。见编辑。结果相同。如果你说你的要夺回标题,那么可能是我的代码的其他部分搞砸了,因为我们有同样的东西。我将摆脱这些,看看它是否仍然会导致问题。感谢大家的帮助!
    • @QrowSaki 为了清楚起见,我最后添加了整个脚本。我认为游戏规则改变者是{ headless: true } 更改为{ headless: false }。值得调查为什么它会导致不同的结果。很高兴我能提供一点帮助。
    • 是的,它是“headful” chrome。问题是:只有在浏览器不是无头的情况下,这个站点才能被自动化/抓取(至少到目前为止它似乎是限制)。您可以尝试将 puppeteer-extra 与名为 stealth 的附加插件一起使用来假装您的 chrome 是一个 headful 实例 - 无需启动 UI:npmjs.com/package/puppeteer-extra-plugin-stealth 如果它值得您付出努力(以及对你的项目)。
    猜你喜欢
    • 1970-01-01
    • 2021-09-22
    • 2017-10-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-12
    • 2017-07-16
    相关资源
    最近更新 更多