繁体   English   中英

Puppeteer 等待页面完全加载

[英]Puppeteer wait until page is completely loaded

我正在从 web 页面创建 PDF。

我正在处理的应用程序是单页应用程序。

我在https://github.com/GoogleChrome/puppeteer/issues/1412上尝试了很多选项和建议

但它不起作用

    const browser = await puppeteer.launch({
    executablePath: 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
    ignoreHTTPSErrors: true,
    headless: true,
    devtools: false,
    args: ['--no-sandbox', '--disable-setuid-sandbox']
});

const page = await browser.newPage();

await page.goto(fullUrl, {
    waitUntil: 'networkidle2'
});

await page.type('#username', 'scott');
await page.type('#password', 'tiger');

await page.click('#Login_Button');
await page.waitFor(2000);

await page.pdf({
    path: outputFileName,
    displayHeaderFooter: true,
    headerTemplate: '',
    footerTemplate: '',
    printBackground: true,
    format: 'A4'
});

我想要的是在页面完全加载后立即生成 PDF 报告。

我不想写任何类型的延迟,即 await page.waitFor(2000);

我不能做 waitForSelector 因为页面有图表和图形,这些图表是在计算后呈现的。

帮助将不胜感激。

您可以使用page.waitForNavigation()在生成 PDF 之前等待新页面完全加载:

await page.goto(fullUrl, {
  waitUntil: 'networkidle0',
});

await page.type('#username', 'scott');
await page.type('#password', 'tiger');

await page.click('#Login_Button');

await page.waitForNavigation({
  waitUntil: 'networkidle0',
});

await page.pdf({
  path: outputFileName,
  displayHeaderFooter: true,
  headerTemplate: '',
  footerTemplate: '',
  printBackground: true,
  format: 'A4',
});

如果您希望将某个动态生成的元素包含在 PDF 中,请考虑使用page.waitForSelector()以确保内容可见:

await page.waitForSelector('#example', {
  visible: true,
});

有时networkidle事件并不总是表明页面已完全加载。 仍然可能有一些JS scripts修改页面上的内容。 因此,观察浏览器完成对HTML源代码的修改似乎会产生更好的结果。 这是您可以使用的功能 -

const waitTillHTMLRendered = async (page, timeout = 30000) => {
  const checkDurationMsecs = 1000;
  const maxChecks = timeout / checkDurationMsecs;
  let lastHTMLSize = 0;
  let checkCounts = 1;
  let countStableSizeIterations = 0;
  const minStableSizeIterations = 3;

  while(checkCounts++ <= maxChecks){
    let html = await page.content();
    let currentHTMLSize = html.length; 

    let bodyHTMLSize = await page.evaluate(() => document.body.innerHTML.length);

    console.log('last: ', lastHTMLSize, ' <> curr: ', currentHTMLSize, " body html size: ", bodyHTMLSize);

    if(lastHTMLSize != 0 && currentHTMLSize == lastHTMLSize) 
      countStableSizeIterations++;
    else 
      countStableSizeIterations = 0; //reset the counter

    if(countStableSizeIterations >= minStableSizeIterations) {
      console.log("Page rendered fully..");
      break;
    }

    lastHTMLSize = currentHTMLSize;
    await page.waitForTimeout(checkDurationMsecs);
  }  
};

您可以在页面load / click函数调用之后和处理页面内容之前使用它。 例如

await page.goto(url, {'timeout': 10000, 'waitUntil':'load'});
await waitTillHTMLRendered(page)
const data = await page.content()

在某些情况下,对我来说最好的解决方案是:

await page.goto(url, { waitUntil: 'domcontentloaded' });

您可以尝试的其他一些选项是:

await page.goto(url, { waitUntil: 'load' });
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.goto(url, { waitUntil: 'networkidle0' });
await page.goto(url, { waitUntil: 'networkidle2' });

您可以在 puppeteer 文档中查看此内容: https ://pptr.dev/#?product=Puppeteer&version=v11.0.0&show=api-pagewaitfornavigationoptions

我总是喜欢等待选择器,因为它们中的许多是页面已完全加载的一个很好的指标:

await page.waitForSelector('#blue-button');

在最新的 Puppeteer 版本中, networkidle2为我工作:

await page.goto(url, { waitUntil: 'networkidle2' });

page.clickpage.waitForNavigation包装在 Promise.all 中

  await Promise.all([
    page.click('#submit_button'),
    page.waitForNavigation({ waitUntil: 'networkidle0' })
  ]);

在使用屏幕外渲染器时,我遇到了与networkidle相同的问题。 我需要一个基于 WebGL 的引擎来完成渲染,然后才能制作屏幕截图。 对我有用的是page.waitForFunction()方法。 在我的情况下,用法如下:

await page.goto(url);
await page.waitForFunction("renderingCompleted === true")
const imageBuffer = await page.screenshot({});

在渲染代码中,我只是在完成renderingCompleted变量设置为 true。 如果您无权访问页面代码,则可以使用其他现有标识符。

您还可以使用来确保所有元素都已渲染

await page.waitFor('*')

参考: https ://github.com/puppeteer/puppeteer/issues/1875

至于 2020 年 12 月, waitFor函数已被弃用,正如代码中的警告所示:

waitFor 已弃用,将在未来版本中删除。 有关详细信息以及如何迁移代码,请参阅https://github.com/puppeteer/puppeteer/issues/6214

您可以使用:

sleep(millisecondsCount) {
    if (!millisecondsCount) {
        return;
    }
    return new Promise(resolve => setTimeout(resolve, millisecondsCount)).catch();
}

并使用它:

(async () => {
    await sleep(1000);
})();

到目前为止的答案还没有提到一个关键事实:不可能编写一个适用于每个页面的万能的waitUntilPageLoaded函数。 如果可能的话,Puppeteer 肯定会提供的。

这样的函数不能依赖超时,因为总有一些页面的加载时间比超时时间长。 当您延长超时以降低失败率时,在使用快速页面时会引入不必要的延迟。 超时通常是一个糟糕的解决方案,选择退出 Puppeteer 的事件驱动模型。

如果响应涉及长时间运行的 DOM 更新(触发渲染需要超过 500 毫秒),则等待空闲网络请求可能并不总是有效。

等待 DOM 停止更改可能会错过缓慢的网络请求、长时间延迟的 JS 触发器或正在进行的 DOM 操作,这可能会导致侦听器永远不会稳定下来,除非经过特殊处理。

当然,还有用户交互:验证码、提示和 cookie/订阅模式,需要在页面处于合理状态之前单击并关闭以获取整页屏幕截图(例如)。

由于每个页面都有不同的、任意的 JS 行为,典型的方法是编写适用于特定页面的事件驱动逻辑。 做出精确的、有针对性的假设比拼凑一大堆试图解决每个边缘情况的黑客要好得多。

如果您的用例编写适用于每个页面的加载事件,我的建议是使用此处描述的最平衡的工具组合以满足您的需求(速度与准确性,开发时间/代码复杂性与准确性, ETC)。 对所有事情都使用故障保险,而不是盲目地假设所有页面都会与您的假设合作。 认真思考您真正需要在多大程度上尝试处理每个网页。 准备妥协并接受你可以忍受的某种程度的失败。


以下是您可以混合搭配以等待负载满足您的需求的策略的简要说明:

page.goto()page.waitForNavigation()默认为load事件,“在整个页面加载完毕时触发,包括样式表和图片等所有依赖资源”( MDN ),但这通常过于悲观; 无需等待大量您不关心的数据。 通常数据无需等待所有外部资源即可获得,因此domcontentloaded应该更快。 请参阅我的帖子避免 Puppeteer 反模式以进行进一步讨论。

另一方面,如果在load之后有 JS 触发的网络请求,您将错过该数据。 因此networkidle2networkidle0 ,它们在活动网络请求的数量为 2 或 0 后等待 500 毫秒。 2 版本的动机是某些站点保持正在进行的请求打开,这会导致networkidle0超时。

如果您正在等待可能具有有效负载的特定网络响应(或者,对于一般情况,实现您自己的网络空闲监视器),请使用page.waitForResponse() page.waitForRequest()page.waitForNetworkIdle()page.on("request", ...)在这里也很有用。

如果您正在等待特定选择器可见,请使用page.waitForSelector() 如果您正在等待特定页面上的加载,请确定一个选择器来指示您要等待的状态。 一般来说,对于特定于一个页面的脚本,这是等待您想要的状态的主要工具,无论您是在提取数据还是单击某些内容。 框架和阴影根阻碍了这个功能。

page.waitForFunction()让您等待任意谓词,例如,检查页面的 HTML 或特定列表是否具有特定长度。 它对于快速进入帧和影子根以等待依赖于嵌套状态的谓词也很有用。 这个函数对于检测 DOM 突变也很方便。

最通用的工具是page.evaluate() ,它将代码插入浏览器。 您可以在此处放置您想要的任何条件; 大多数其他 Puppeteer 函数都是常见情况的便捷包装器,您可以使用evaluate手动实现。

我不能发表评论,但我为任何认为有用的人(即如果他们使用 pyppeteer)制作了 Anand 答案的 python 版本。

async def waitTillHTMLRendered(page: Page, timeout: int = 30000): 
    check_duration_m_secs = 1000
    max_checks = timeout / check_duration_m_secs
    last_HTML_size = 0
    check_counts = 1
    count_stable_size_iterations = 0
    min_stabe_size_iterations = 3

    while check_counts <= max_checks:
        check_counts += 1
        html = await page.content()
        currentHTMLSize = len(html); 

        if(last_HTML_size != 0 and currentHTMLSize == last_HTML_size):
            count_stable_size_iterations += 1
        else:
            count_stable_size_iterations = 0 # reset the counter

        if(count_stable_size_iterations >= min_stabe_size_iterations):
            break
    

        last_HTML_size = currentHTMLSize
        await page.waitFor(check_duration_m_secs)

请记住,没有灵丹妙药可以处理所有页面加载,一种策略是监视 DOM,直到它稳定(即没有看到突变)超过n毫秒。 这类似于网络空闲解决方案,但面向 DOM 而不是请求,因此涵盖了加载行为的不同子集。

通常,此代码将遵循page.waitForNavigation({waitUntil: "domcontentloaded"})page.goto(url, {waitUntil: "domcontentloaded"}) ,但您也可以在旁边等待它,例如waitForNetworkIdle()使用Promise.all()Promise.race()

这是一个简单的例子:

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

const waitForDOMStable = (
  page,
  options={timeout: 30000, idleTime: 2000}
) =>
  page.evaluate(({timeout, idleTime}) =>
    new Promise((resolve, reject) => {
      setTimeout(() => {
        observer.disconnect();
        const msg = `timeout of ${timeout} ms ` +
          "exceeded waiting for DOM to stabilize";
        reject(Error(msg));
      }, timeout);
      const observer = new MutationObserver(() => {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(finish, idleTime);
      });
      const config = {
        attributes: true,
        childList: true,
        subtree: true
      };
      observer.observe(document.body, config);
      const finish = () => {
        observer.disconnect();
        resolve();
      };
      let timeoutId = setTimeout(finish, idleTime);
    }),
    options
  )
;

const html = `<!DOCTYPE html><html lang="en"><head>
<title>test</title></head><body><h1></h1><script>
(async () => {
  for (let i = 0; i < 10; i++) {
    document.querySelector("h1").textContent += i + " ";
    await new Promise(r => setTimeout(r, 1000));
  }
})();
</script></body></html>`;

let browser;
(async () => {
  browser = await puppeteer.launch({headless: true});
  const [page] = await browser.pages();
  await page.setContent(html);
  await waitForDOMStable(page);
  console.log(await page.$eval("h1", el => el.textContent));
})()
  .catch(err => console.error(err))
  .finally(() => browser?.close())
;

对于持续改变 DOM 的频率高于idle值的页面,超时最终将触发并拒绝承诺,遵循典型的 Puppeteer 回退。 您可以设置更积极的整体超时以满足您的需求,或者定制逻辑以忽略(或仅监控)特定子树。

对我来说, { waitUntil: 'domcontentloaded' }始终是我的 go。 我发现networkidle不能正常工作...

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM