【问题标题】:Trouble modifying an object within page.evaluate in Puppeteer在 Puppeteer 中修改 page.evaluate 中的对象时遇到问题
【发布时间】:2019-12-22 20:09:41
【问题描述】:

我正在创建一个 Twitter 抓取工具作为一个项目。向下滚动时,推文会在 DOM 中呈现,因此我想使用 Puppeteer 滚动、提取数据并将其保存到预定义的对象中,然后继续滚动。问题是脚本实际上并没有修改提供的对象,我只剩下一个空对象。

提取数据的 for 循环在滚动函数外部调用时起作用(即,我可以提取页面中呈现的第一条推文)。滚动功能本身有效,我是从Puppeteer - scroll down until you can't anymore 获得的。

出于测试目的,我将滚动功能设置为仅滚动 20 次(否则它被设计为滚动直到无法再滚动)。这是我的代码:

app.get('/scrape', async (req, res) => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.setJavaScriptEnabled(true)
    await page.goto(`https://twitter.com/${req.query.url}`);
    await page.setJavaScriptEnabled(true)
    let obj = {}
    await autoScroll(page, obj)
    async function autoScroll(page, obj) {
        await page.evaluate(async (obj) => {
            await new Promise((resolve, reject) => {
                var totalHeight = 0;
                var distance = 400;
                var count = 0
                var timer = setInterval(() => {
                    var scrollHeight = document.body.scrollHeight;
                    window.scrollBy(0, distance);
                    totalHeight += distance;
                    for (let i = 0; i < 100; i++) {
                        let id, date, text
                        try {
                            id = document.body.childNodes[7].childNodes[3].childNodes[1].childNodes[5].childNodes[1].childNodes[1].childNodes[3].childNodes[1].childNodes[3].childNodes[7].childNodes[1].childNodes[3].childNodes[1].childNodes[i].childNodes[1].getAttribute('data-tweet-id')
                            date = document.body.childNodes[7].childNodes[3].childNodes[1].childNodes[5].childNodes[1].childNodes[1].childNodes[3].childNodes[1].childNodes[3].childNodes[7].childNodes[1].childNodes[3].childNodes[1].childNodes[i].childNodes[1].childNodes[3].childNodes[1].childNodes[3].childNodes[1].getAttribute('title')
                            text = document.body.childNodes[7].childNodes[3].childNodes[1].childNodes[5].childNodes[1].childNodes[1].childNodes[3].childNodes[1].childNodes[3].childNodes[7].childNodes[1].childNodes[3].childNodes[1].childNodes[i].childNodes[1].childNodes[3].childNodes[3].childNodes[1].innerHTML
                            obj[id] = { date: date, text: text }
                            console.log(i)
                        } catch (err) { continue }
                    }
                    count++
                    //if(totalHeight >= scrollHeight){
                    if (count === 20) {
                        clearInterval(timer);
                        resolve();
                    }
                }, 400);
            });
        }, obj);
    }
    res.send(obj)
    await browser.close();
})

请求每次发送一个空对象。我没有收到任何错误消息或控制台日志;如果它们在那里,我看不到它们,因为它们是在 Puppeteer 生成的无头 Chrome 浏览器的上下文中执行的。

任何帮助将不胜感激!

【问题讨论】:

  • 你的节点代码使用像 babel 这样的转译器吗?
  • 不适用于这个文件,不。我的前端是 React JSX,所以我用 babel 编译它。

标签: javascript puppeteer


【解决方案1】:

您传递给 page.evaluate 的参数将被 JSON 序列化并传输到页面上下文。

您在page.evaluate() 函数中分配给obj 的属性只会出现在页面上下文中,而不是出现在您调用page.evaluate 的脚本中。

您可以通过从函数返回 obj 对象而不是将其作为参数传递来解决此问题:

let obj = await page.evaluate(async() => {
  return new Promise(resolve => {
      let obj = {};
      // ...
      // set something on obj
      obj['foo'] = 'bar';

      // resolve with the obj
      resolve(obj);
      // ...
  });
});

集成在你的代码 sn-p:

app.get('/scrape', async (req, res) => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.setJavaScriptEnabled(true)
    await page.goto(`https://twitter.com/${req.query.url}`);
    await page.setJavaScriptEnabled(true)
    let obj = await autoScroll(page);
    async function autoScroll(page) {
        return page.evaluate(async () => {
            let obj = {};
            return new Promise((resolve, reject) => {
                var totalHeight = 0;
                var distance = 400;
                var count = 0
                var timer = setInterval(() => {
                    var scrollHeight = document.body.scrollHeight;
                    window.scrollBy(0, distance);
                    totalHeight += distance;
                    for (let i = 0; i < 100; i++) {
                        let id, date, text
                        try {
                            id = document.body.childNodes[7].childNodes[3].childNodes[1].childNodes[5].childNodes[1].childNodes[1].childNodes[3].childNodes[1].childNodes[3].childNodes[7].childNodes[1].childNodes[3].childNodes[1].childNodes[i].childNodes[1].getAttribute('data-tweet-id')
                            date = document.body.childNodes[7].childNodes[3].childNodes[1].childNodes[5].childNodes[1].childNodes[1].childNodes[3].childNodes[1].childNodes[3].childNodes[7].childNodes[1].childNodes[3].childNodes[1].childNodes[i].childNodes[1].childNodes[3].childNodes[1].childNodes[3].childNodes[1].getAttribute('title')
                            text = document.body.childNodes[7].childNodes[3].childNodes[1].childNodes[5].childNodes[1].childNodes[1].childNodes[3].childNodes[1].childNodes[3].childNodes[7].childNodes[1].childNodes[3].childNodes[1].childNodes[i].childNodes[1].childNodes[3].childNodes[3].childNodes[1].innerHTML
                            obj[id] = { date: date, text: text }
                            console.log(i)
                        } catch (err) { continue }
                    }
                    count++
                    //if(totalHeight >= scrollHeight){
                    if (count === 20) {
                        clearInterval(timer);
                        resolve(obj);
                    }
                }, 400);
            });
        });
    }
    res.send(obj)
    await browser.close();
})

如果您使用的是像 babel 这样的转译器,您可能需要将函数作为字符串传递给 page.evaluate,例如:

await page.evaluate(`async() => {
  return Promise.resolve(42);
}`);

(puppeteer 将在您的函数上调用 .toString() 以获取源代码,其中可能包含 references to helpers used by babel,它们不存在于页面上下文中)

编辑:
要调试您的选择器,您可以尝试在non-headless mode 中启动 puppeteer。 通过这种方式,您可以获得一个真正的浏览器窗口,您可以在其中访问开发控制台。 例如:

const browser = await puppeteer.launch({headless: false});

【讨论】:

  • 谢谢,我没有使用 babel 转译这个文件。我实现了你的代码,它返回未定义。我添加了另一个返回语句“return await new Promise((resolve, reject) => {”,结果得到一个空对象。
  • 经过进一步测试后,我尝试将 obj.a = 1 添加到我的 for 循环中,它确实返回了,所以我认为您的代码正在运行,而我的 try/catch 未能返回任何数据.我应该在另一个评估函数中使用 id = await page.evaluate(i => document.body.childNode[7]... , i) 吗?我不确定我的逻辑问题出在哪里。
  • @NickP 你说得对,我忘了添加退货,修复它。您可以尝试删除 try / catch 并查看您遇到的错误。可能您的.childNodes[] 之一返回null,因为该元素不存在。如果您可以使用 css 选择器定位您想要的节点,您也可以使用 document.querySelectordocument.querySelectorAll
  • 我尝试使我的 setInterval 异步,将页面正确地传递到我的评估函数中,并使用“id = await page.evaluate(i => document.body.childNode[7]..., i )" 格式。我收到一个循环 JSON 错误。至于您的评论,不幸的是没有选择器可以定位。当我没有将它们包裹在滚动函数中时,嵌套的 childNodes 正在工作,所以我不确定。我会继续玩它。遗憾的是,我在评估函数中看不到具体错误,因为它们出现在不可见的无头 Chrome 实例中,这使得调试特别困难。
  • @NickP 你不能将page 传递给评估。您传递给 evaluate 的所有参数必须是 JSON 可序列化(普通对象或原语)或 JSHandles(引用页面中对象的句柄)
猜你喜欢
  • 2021-07-13
  • 2018-03-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-14
  • 1970-01-01
相关资源
最近更新 更多