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

时间:2017-09-19 06:20:21

标签: web-scraping google-chrome-headless puppeteer

我有

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

这就是我正在做的事情

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

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

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

5 个答案:

答案 0 :(得分:16)

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

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

const urls = [...]

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

这会像你期待的那样访问一个接一个的网址。如果您对使用await / async进行串行迭代感到好奇,可以看一下这个答案:https://stackoverflow.com/a/24586168/791691

答案 1 :(得分:1)

accepted answer显示了如何一次连续访问每一页。但是,当任务为embarrassingly parallel时,您可能希望同时访问多个页面,也就是说,抓取特定页面并不依赖于从其他页面提取的数据。

Promise.allSettled是一个可以帮助实现这一目标的工具,它使我们可以立即兑现许诺,确定哪些成功并收获结果。

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

序列号:

const puppeteer = require("puppeteer");

(async () => {
  const browser = await puppeteer.launch({dumpio: false});
  const baseURL = "https://stackoverflow.com/users";
  const startId = 6243352;
  const qty = 40;
  const usernames = [];
  
  for (let i = startId; i < startId + qty; i++) {
    await page.goto(`${baseURL}/${i}`);
  
    try {
      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 = 40;

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

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

这里的示例是人为设计的,因为它没有与页面动态交互,因此没有像典型的Puppeteer用例那样需要大量的获取空间,该用例涉及网络请求并阻止每页的等待。

当然,要提防速率限制和网站施加的任何其他限制。

对于每个任务创建page的成本过高或要为并行请求分派设置上限的任务,请考虑使用任务队列。

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

答案 2 :(得分:0)

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

SET OutputLocalEnvironment.Destination.SOAP.Request.Operation = 'myOperation';

根据此github issue

的引用

答案 3 :(得分:0)

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

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

    }
})();

答案 4 :(得分:0)

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

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