使用 Puppeteer 循环抓取多个 URL

新手上路,请多包涵

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

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

这就是我正在做的:

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

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

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

原文由 ahhmarr 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1.4k
2 个回答

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

原文由 tomahaug 发布,翻译遵循 CC BY-SA 4.0 许可协议

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

可以帮助实现这一点的工具是 Promise.allSettled 它可以让我们立即发出一堆承诺,确定哪些是成功的并收获结果。

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

串行码:

 const puppeteer = require("puppeteer"); // ^14.3.0

let browser;
(async () => {
  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++) {
    await page.goto(`${baseURL}/${i}`, {
      waitUntil: "domcontentloaded"
    });
    const sel = ".flex--item.mb12.fs-headline2.lh-xs";
    const el = await page.waitForSelector(sel);
    usernames.push(await el.evaluate(el => el.textContent.trim()));
  }

  console.log(usernames);
})()
  .catch(err => console.error(err))
  .finally(() => browser.close())
;

并行代码:

 const puppeteer = require("puppeteer");

let browser;
(async () => {
  browser = await puppeteer.launch();
  const [page] = await browser.pages();
  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}`, {
        waitUntil: "domcontentloaded"
      });
      const sel = ".flex--item.mb12.fs-headline2.lh-xs";
      const el = await page.waitForSelector(sel);
      const text = await el.evaluate(el => el.textContent.trim());
      await page.close();
      return text;
    })))
    .filter(e => e.status === "fulfilled")
    .map(e => e.value)
  ;
  console.log(usernames);
})()
  .catch(err => console.error(err))
  .finally(() => browser.close())
;

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

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

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

对于创建 page 每个任务的任务成本过高,或者您想为并行请求分派设置上限,请考虑使用任务队列或组合上面显示的串行和并行代码以分块发送请求。 这个答案 显示了这个 Puppeteer 不可知论者的通用模式。

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

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

原文由 ggorlen 发布,翻译遵循 CC BY-SA 4.0 许可协议

推荐问题