使用 Puppeteer 循环抓取多个 URL

Crawling multiple URLs in a loop using Puppeteer

我有一组 URL 可以从中抓取数据:

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

这就是我正在做的事情:

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

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

我想知道我是不是做错了什么,或者这种功能不是 advised/supported。

mapforEachreduce 等不等待它们内部的异步操作,在它们继续进行迭代的迭代器的下一个元素之前。

有多种方法可以在执行异步操作时同步遍历迭代器的每个项目,但我认为在这种情况下最简单的方法是简单地使用普通的 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 进行串行迭代感到好奇,你可以看看这个答案:

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

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 issue

我找到的实现此目的的最佳方法。

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

    }
})();

shows how to serially visit each page one at a time. However, you may want to visit multiple pages simultaneously when the task is embarrassingly parallel,即抓取特定页面不依赖于从其他页面提取的数据。

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

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

序列号:

const puppeteer = require("puppeteer");

(async () => {
  const browser = await puppeteer.launch({dumpio: false});
  const [page] = await browser.pages();
  const baseURL = "https://whosebug.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://whosebug.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 不可知论者的通用模式。

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

另见 ,它解释了为什么在此线程中使用 map 的原始尝试未能等待每个承诺。

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

  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  page.setDefaultNavigationTimeout(0);