【问题标题】:Is there a way to get puppeteer's waitUntil "networkidle" to only consider XHR (ajax) requests?有没有办法让 puppeteer 的 waitUntil "networkidle" 只考虑 XHR (ajax) 请求?
【发布时间】:2021-02-07 04:39:18
【问题描述】:

我正在使用 puppeteer 在我的测试应用程序中评估基于 javascript 的网页 HTML。

这是我用来确保加载所有数据的行:

await page.setRequestInterception(true);
page.on("request", (request) => {
  if (request.resourceType() === "image" || request.resourceType() === "font" || request.resourceType() === "media") {
    console.log("Request intercepted! ", request.url(), request.resourceType());
    request.abort();
  } else {
    request.continue();
  }
});
try {
  await page.goto(url, { waitUntil: ['networkidle0', 'load'], timeout: requestCounterMaxWaitMs });
} catch (e) {

}

这是等待 ajax 请求 完成的最佳方式吗?

感觉不错,但我不确定是否应该使用 networkidle0、networkidle1 等?

【问题讨论】:

    标签: node.js puppeteer


    【解决方案1】:

    您可以使用pending-xhr-puppeteer,这是一个公开承诺等待所有待处理的 xhr 请求得到解决的库。

    像这样使用它:

    const puppeteer = require('puppeteer');
    const { PendingXHR } = require('pending-xhr-puppeteer');
    
    const browser = await puppeteer.launch({
      headless: true,
      args,
    });
    
    const page = await browser.newPage();
    const pendingXHR = new PendingXHR(page);
    await page.goto(`http://page-with-xhr`);
    // Here all xhr requests are not finished
    await pendingXHR.waitForAllXhrFinished();
    // Here all xhr requests are finished
    

    免责声明:我是pending-xhr-puppeteer的维护者

    【讨论】:

    • 真是太棒了!我会尽可能尝试一下,然后告诉你。我们的主要用例是 javascript 评估的网络爬虫,这是一门非常微妙的科学,“不要等待太久”,但同时要“等待足够长”。你觉得这个库有帮助吗?
    • 嗨@NicholasDiPiazza。我在 react/graphql 应用程序上使用它来等待它的满载。今天,这个库只等待“直接”触发的 xhr。该库不应捕获来自setTimeoutsetInterval 的xhr。该库没有等待 xhr 但“不超过”的功能。为此,您可以使用 Promise.race 和 setTimeout。我将在这个案例的自述文件中添加一个示例。
    • 这看起来非常好,但它不能很好地处理多个页面。如果我在开始时添加到页面,那么它似乎无法正常工作(有 xhr 请求“徘徊”。如果我只将它添加到特定函数中,那么我们会遇到 maxlisteners 错误和内存泄漏。那里需要一种在使用后“删除”监听器的简单方法。
    • 你说得对,我不会在页面更改时刷新监听器。我刚刚创建了一个问题。谢谢
    【解决方案2】:

    XHR 本质上可以稍后出现在应用程序中。如果应用程序在例如 1 秒后发送 XHR 并且您想等待它,则任何 networkidle0 都不会帮助您。我认为,如果您想“正确地”执行此操作,您应该知道您正在等待什么请求以及await

    这是一个示例,其中 XHR 稍后在应用程序中发生,它会等待所有这些:

    const puppeteer = require('puppeteer');
    
    const html = `
    <html>
      <body>
        <script>
          setTimeout(() => {
            fetch('https://swapi.co/api/people/1/');
          }, 1000);
    
          setTimeout(() => {
            fetch('https://www.metaweather.com/api/location/search/?query=san');
          }, 2000);
    
          setTimeout(() => {
            fetch('https://api.fda.gov/drug/event.json?limit=1');
          }, 3000);
        </script>
      </body>
    </html>`;
    
    // you can listen to part of the request
    // in this example I'm waiting for all of them
    const requests = [
        'https://swapi.co/api/people/1/',
        'https://www.metaweather.com/api/location/search/?query=san',
        'https://api.fda.gov/drug/event.json?limit=1'
    ];
    
    const waitForRequests = (page, names) => {
      const requestsList = [...names];
      return new Promise(resolve =>
         page.on('request', request => {
           if (request.resourceType() === "xhr") {
             // check if request is in observed list
             const index = requestsList.indexOf(request.url());
             if (index > -1) {
               requestsList.splice(index, 1);
             }
    
             // if all request are fulfilled
             if (!requestsList.length) {
               resolve();
             }
           }
           request.continue();
         })
      );
    };
    
    
    (async () => {
      const browser = await puppeteer.launch();
      const page = await browser.newPage();
      await page.setRequestInterception(true);
    
      // register page.on('request') observables
      const observedRequests = waitForRequests(page, requests);
    
      // await is ignored here because you want to only consider XHR (ajax) 
      // but it's not necessary
      page.goto(`data:text/html,${html}`);
    
      console.log('before xhr');
      // await for all observed requests
      await observedRequests;
      console.log('after all xhr');
      await browser.close();
    })();
    

    【讨论】:

    • 所以,这是通过定义请求 URL 来实现的。但是任意网络请求呢?比如 url 是否包含创建资源时生成的随机 uuid?
    • @otong 它完全是临时的,因此您可以将 requestsList.indexOf(request.url()); 替换为定义您期望的资源的任何模式逻辑。例如,如果您有一个 10 个字母数字字符的 uuid,您可能会使用 /^http:\/\/www.example.com\/[a-zA-Z\d]{10}$/.test(request.url()) 之类的东西,并保留一个数据结构/标志来判断它是否之前被请求过(如果这对您的用例很重要)。
    • 除了这个答案之外,Puppeteer 还提供了page.waitForResponse 来等待特定的响应来解决,而不必承诺page.on("request", ...),尽管很高兴看到多种方法。
    • 另外,在这种情况下,最好使用page.once("request", ...) 而不是page.on("request", ...),这样处理程序在触发一次后就会被删除。
    【解决方案3】:

    我同意 this answer 中的观点,即等待所有网络活动停止(“所有数据都已加载”)是一个相当模糊的概念,完全取决于您正在抓取的网站。

    检测响应的选项包括等待固定持续时间、网络流量空闲后的固定持续时间、特定响应(或一组响应)、页面上出现的元素、返回 true 的谓词等,都是Puppeteer supports

    考虑到这一点,最典型的情况是,您正在等待某个特定响应或一组来自已知(或部分已知,使用某种模式或前缀)资源 URL 的响应,这些 URL 将传递有效负载您想要读取和/或触发您需要检测的 DOM 交互。 Puppeteer 提供 page.waitForResponse 来做这件事。

    这是一个基于existing answer 的示例(并展示了我们如何从响应中检索数据):

    const puppeteer = require("puppeteer");
    
    const html = `
    <html>
      <body>
        <script>
          setTimeout(() => {
            fetch("http://jsonplaceholder.typicode.com/users/1");
          }, 1000);
          setTimeout(() => {
            fetch("http://jsonplaceholder.typicode.com/users/2");
          }, 2000);
          setTimeout(() => {
            fetch("http://jsonplaceholder.typicode.com/users/3");
          }, 3000);
          setTimeout(() => {
            // fetch something irrelevant to us
            fetch("http://jsonplaceholder.typicode.com/users/4");
          }, 0);
        </script>
      </body>
    </html>`;
    
    (async () => {
      const browser = await puppeteer.launch();
      const [page] = await browser.pages();
      await page.setContent(html);
      const expectedUrls = [
        "http://jsonplaceholder.typicode.com/users/1",
        "http://jsonplaceholder.typicode.com/users/2",
        "http://jsonplaceholder.typicode.com/users/3",
      ];
    
      try {
        const responses = await Promise.all(expectedUrls.map(url =>
          page.waitForResponse(
            response => response.url() === url, 
            {timeout: 5000}
          )
        ));
        const data = await Promise.all(
          responses.map(response => response.json())
        );
        console.log(data);
      }
      catch (err) {
        console.error(err);
      }
    
      await browser.close();
    })()
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-04-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多