25

我有一组 URL 可以从以下位置抓取数据:

urls = ['url','url','url'...]

这就是我正在做的事情:

urls.map(async (url)=>{
  await page.goto(url);
  await page.waitForNavigation({ waitUntil: 'networkidle' });
})

这似乎不等待页面加载并很快访问所有 URL(我什至尝试使用page.waitFor)。

我想知道我是否在做一些根本错误的事情,或者不建议/支持这种类型的功能。

4

5 回答 5

33

map, forEach,reduce等,在它们继续迭代它们正在迭代的迭代器的下一个元素之前,不会等待它们内部的异步操作。

在执行异步操作时,有多种方法可以同步遍历迭代器的每个项目,但在这种情况下,我认为最简单的方法是简单地使用普通for运算符,它会等待操作完成。

const urls = [...]

for (let i = 0; i < urls.length; i++) {
    const url = urls[i];
    await page.goto(`${url}`);
    await page.waitForNavigation({ waitUntil: 'networkidle2' });
}

正如您所期望的那样,这将一个接一个地访问 URL。如果您对使用 await/async 进行串行迭代感到好奇,可以看看这个答案:https ://stackoverflow.com/a/24586168/791691

于 2017-09-19T10:02:11.873 回答
6

接受的答案显示了如何一次一个地连续访问每个页面。但是,当任务非常并行时,您可能希望同时访问多个页面,也就是说,抓取特定页面不依赖于从其他页面提取的数据。

一个可以帮助实现这一目标的工具是Promise.allSettled让我们一次触发一堆 Promise,确定哪些是成功的并收获结果。

举一个基本的例子,假设我们想为给定一系列 id 的 Stack Overflow 用户抓取用户名。

串行码:

const puppeteer = require("puppeteer");

(async () => {
  const browser = await puppeteer.launch({dumpio: false});
  const [page] = await browser.pages();
  const baseURL = "https://stackoverflow.com/users";
  const startId = 6243352;
  const qty = 5;
  const usernames = [];
  
  for (let i = startId; i < startId + qty; i++) {
    try {
      await page.goto(`${baseURL}/${i}`);
      usernames.push(await page.$eval(
        ".profile-user--name", 
        el => el.children[0].innerText
      ));
    }
    catch (err) {}
  }

  console.log(usernames.length);
  await browser.close();
})();

并行代码:

const puppeteer = require("puppeteer");

(async () => {
  const browser = await puppeteer.launch({dumpio: false});
  const baseURL = "https://stackoverflow.com/users";
  const startId = 6243352;
  const qty = 5;

  const usernames = (await Promise.allSettled(
    [...Array(qty)].map(async (_, i) => {
      const page = await browser.newPage();
      await page.goto(`${baseURL}/${i + startId}`);
      return page.$eval(
        ".profile-user--name", 
        el => el.children[0].innerText
      );
    })))
    .filter(e => e.status === "fulfilled")
    .map(e => e.value)
  ;
  console.log(usernames.length);
  await browser.close();
})();

请记住,这是一种技术,而不是保证所有工作负载速度提高的灵丹妙药。需要进行一些实验才能在给定特定任务和系统上创建更多页面的成本与网络请求的并行化之间找到最佳平衡。

此处的示例是人为设计的,因为它没有与页面动态交互,因此没有像典型的 Puppeteer 用例那样有很大的收益空间,该用例涉及网络请求和每个页面的阻塞等待。

当然,请注意速率限制和站点施加的任何其他限制(运行上面的代码可能会激怒 Stack Overflow 的速率限制器)。

对于创建page每个任务非常昂贵的任务,或者您想设置并行请求调度的上限,请考虑使用任务队列或结合上面显示的串行和并行代码以分块发送请求。这个答案显示了这个 Puppeteer 不可知论者的通用模式。

这些模式可以扩展以处理某些页面依赖于其他页面的数据的情况,形成依赖图

另请参阅将 async/await 与 forEach 循环一起使用,它解释了为什么在此线程中使用的原始尝试map无法等待每个承诺。

于 2020-11-25T07:19:40.520 回答
2

如果您发现您无限期地等待您的承诺,建议的解决方案是使用以下内容:

const urls = [...]

for (let i = 0; i < urls.length; i++) {
    const url = urls[i];
    const promise = page.waitForNavigation({ waitUntil: 'networkidle' });
    await page.goto(`${url}`);
    await promise;
}

从这个github 问题中引用

于 2019-01-15T13:48:09.333 回答
0

没有人提到的是,如果您使用同一个页面对象获取多个页面,则将其超时设置为 0 至关重要。否则,一旦它获取了默认的 30 秒页面,它将超时。

  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  page.setDefaultNavigationTimeout(0);
于 2021-05-05T02:27:24.143 回答
-1

我发现实现这一目标的最佳方法。

 const puppeteer = require('puppeteer');
(async () => {
    const urls = ['https://www.google.com/', 'https://www.google.com/']
    for (let i = 0; i < urls.length; i++) {

        const url = urls[i];
        const browser = await puppeteer.launch({ headless: false });
        const page = await browser.newPage();
        await page.goto(`${url}`, { waitUntil: 'networkidle2' });
        await browser.close();

    }
})();
于 2020-04-23T03:33:40.253 回答