使用puppeteer在循环中抓取多个URL

ahh*_*arr 18 web-scraping google-chrome-headless puppeteer

我有

urls = ['url','url','url'...]
Run Code Online (Sandbox Code Playgroud)

这就是我正在做的事情

urls.map(async (url)=>{
  await page.goto(`${url}`);
  await page.waitForNavigation({ waitUntil: 'networkidle' });
})
Run Code Online (Sandbox Code Playgroud)

这似乎不等待页面加载并快速访问所有网址(我甚至尝试使用page.waitFor)

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

tom*_*aug 21

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: 'networkidle' });
}
Run Code Online (Sandbox Code Playgroud)

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


ggo*_*len 12

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

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

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

串行码:

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

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 = [];

  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());
Run Code Online (Sandbox Code Playgroud)

并行代码:

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());
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

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