使用 Puppeteer 定时抓取网页截图

问题描述 投票:0回答:3

我有一个 Puppeteer 功能,可以通过在设定的时间间隔截屏来回放网站的加载:

const getScreenshots = async (browser, url, ms, frames): Promise<string[]> => {
  const page = await browser.newPage()

  // Set screen size
  await page.setViewport({ width: 1280, height: 800 })

  await page.goto(url, {
    waitUntil: "networkidle0",
  })

  const promises = []
  return new Promise((resolve) => {
    const intervalId = setInterval(async () => {
      promises.push(
        page.screenshot({
          captureBeyondViewport: true,
          fullPage: true,
          encoding: "base64",
        })
      )
      if (promises.length >= frames) {
        clearInterval(intervalId)
        resolve(promises)
      }
    }, ms)
  })
}

const screenshotTest = async (url: string): Promise<string> => {
  const browser = await puppeteer.launch({ timeout: 100000 })
  try {
    const imgArray: any[] = await getScreenshots(browser, url, 42, 24)
    return imgArray[imgArray.length - 1]
  } finally {
    // await browser.close()
  }
}

只要

await browser.close()
行仍然被注释掉,这就可以正常工作,但这允许僵尸进程在请求完成后继续运行。当我实际进行此调用时,该函数抛出,因为浏览器在 promise 解决之前被关闭。这显然是因为该函数正在异步运行,尽管我预计它会在
finally
块运行之前等待承诺解决,因为
try
块的返回值正在等待它们,但显然这不是案例。

如何在浏览器关闭之前重写它以等待承诺?

我相信我可以做这样的事情:

while (true) {
  if (Promise.all(promises)) {
    await browser.close()
  }
}

但是像这样继续循环似乎效率很低所以我希望我错过了一些简单的方法来等待承诺,然后关闭浏览器。

javascript web-scraping async-await puppeteer screen-scraping
3个回答
0
投票

考虑将您的

getScreenshots()
功能更改为类似于:

const getScreenshots = async (browser, url, ms, frames) => {
  const promises = []
  const page = await browser.newPage()

  return page.goto(url).then(async function capture() {
    await new Promise(r => setTimeout(r, ms)) // delay
    promises.push(page.screenshot())
    if (promises.length < frames) {
      return capture()
    }
    const screenshots = await Promise.all(promises)
    page.close() // await should not be needed
    return screenshots
  })
}

这是一个类似的端到端测试:

import puppeteer from 'puppeteer'

const url = 'https://fetch-progress.anthum.com/30kbps/images/sunrise-progressive.jpg'
let browser

try {
  console.log('capturing', url)
  browser = await puppeteer.launch()
  const screenshots = await getAllScreenshots(browser, url, 500)
  console.log('captured', screenshots.length)
} finally {
  browser?.close()
}

// returns Promise<screenshot[]>
async function getAllScreenshots(browser, url, captureInterval) {
  const page = await browser.newPage()

  // Resolves when page load completes and all screenshots complete
  return new Promise(async (resolve, reject) => {
    const all = []
    let timeoutId = 1

    page.on('error', e => {
      timeoutId = clearTimeout(timeoutId)
      reject(e)
    })
    page.on('load', () => {
      timeoutId = clearTimeout(timeoutId)
      resolve(capture(page, all)) // capture final screenshot
    })

    page.goto(url)

    while (timeoutId) {
      await Promise.all([
        capture(page, all),
        new Promise(r => timeoutId = setTimeout(r, captureInterval))
      ])
    }
  }).finally(() => page.close())
}

// returns Promise<screenshot[]> after adding it to @all
async function capture(page, all) {
  all.push(await page.screenshot())
  return all
}


0
投票

你不是在等待承诺。 您正在等待对数组的承诺,该数组又包含所有屏幕截图承诺。 为此使用

Promise.all

await Promise.all(arrayOfPromises);

或者在你的情况下:

await (await getScreenshots(browser, url, 42, 24))

从那里你可以看出,将整个事情包装成

new Promise
开始是没有意义的。


-2
投票

这似乎是

new Promise
反模式 的一个例子。最近的 Node 版本提供了一个 promisified
setTimeout
setInterval
让你避免回调。

例如,对于

setTimeout

const {setTimeout} = require("node:timers/promises");

const getScreenshots = async (
  browser,
  url,
  ms,
  frames
): Promise<string[]> => {
  const page = await browser.newPage();
  await page.setViewport({width: 1280, height: 800});
  await page.goto(url, {waitUntil: "networkidle0"});

  const screenshots = [];

  for (let i = 0; i < frames; i++) {
    const screenshot = await page.screenshot({
      captureBeyondViewport: true,
      fullPage: true,
      encoding: "base64",
    });
    screenshots.push(screenshot);
    await setTimeout(ms);
  }

  return screenshots;
};

setInterval

const {setInterval} = require("node:timers/promises");

const getScreenshots = async (
  browser,
  url,
  ms,
  frames
): Promise<string[]> => {
  const page = await browser.newPage();
  await page.setViewport({width: 1280, height: 800});
  await page.goto(url, {waitUntil: "networkidle0"});

  const screenshots = [];

  for await (const startTime of setInterval(ms)) {
    const screenshot = await page.screenshot({
      captureBeyondViewport: true,
      fullPage: true,
      encoding: "base64",
    });
    screenshots.push(screenshot);

    if (screenshots.length >= frames) {
      return screenshots;
    }
  }
};

调用代码相同,

browser.close()
没有注释

请注意,任何带有

setTimeout
setInterval
的解决方案都会随时间漂移。无论如何,截屏是一个复杂的、非即时的子进程调用,所以我想尝试这样做的回报会递减,但你可以尝试一个带有
requestAnimationFrame
调用和漂移校正的紧密
performance.now()
循环。

除了

new Promise
之外,其他红旗在没有
async
的函数上使用
await
async
上使用
new Promise
并进行
setInterval
回调
 async
。即使您没有较新的 Node 版本或没有
utils.promisify
(例如在浏览器中),最好将 promisification 埋在一次性函数中并保持主线代码无回调:

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

其他小建议:

  • 有点奇怪的是,你创建了一个截图数组,然后将它们全部扔掉,除了最后一个(你可以更习惯地使用
    .at(-1)
    访问数组的最后一个元素)。
  • 当你有多个参数时,如
    getScreenshots(browser, url, 42, 24)
    ,推荐的方法是切换到配置对象,如
    getScreenshots(browser, url, {ms: 42, frames: 24})
    以保持代码可读性。
  • 我通常更喜欢我的 Puppeteer 助手函数来接受
    page
    而不是整个浏览器。这允许最大的可重用性,因为被调用者不必创建新页面。调用者可以在屏幕截图调用之前在页面上设置任何设置和 URL,而不是将它们作为参数传递。

这是一个完整的、可运行的示例,应用了上述建议:

const fs = require("node:fs/promises");
const puppeteer = require("puppeteer");
const {setInterval} = require("timers/promises");

const getScreenshots = async (page, opts = {ms: 1000, frames: 10}) => {
  const screenshots = [];

  for await (const startTime of setInterval(opts.ms)) {
    const screenshot = await page.screenshot({
      captureBeyondViewport: true,
      fullPage: true,
      encoding: "base64",
    });
    screenshots.push(screenshot);

    if (screenshots.length >= opts.frames) {
      return screenshots;
    }
  }
};

// Driver code for testing:
const html = `<!DOCTYPE html>
<html>
<body>
<h1></h1>
<script>
let i = 0;
setInterval(() => {
  document.querySelector("h1").textContent = ++i;
}, 10);
</script>
</body>
</html>
`;

let browser;
(async () => {
  browser = await puppeteer.launch();
  const [page] = await browser.pages();
  await page.setContent(html);
  const screenshots = await getScreenshots(page, {ms: 100, frames: 10});
  console.log(screenshots.length); // => 10
  const gallery = `<!DOCTYPE html><html><body>
  ${screenshots.map(e => `
    <img alt="test screenshot" src="data:image/png;base64,${e}">
  `)}
  </body></html>`;
  await fs.writeFile("test.html", gallery);
})()
  .catch(err => console.error(err))
  .finally(() => browser?.close());

在浏览器中打开

test.html
以查看 10 个不同时间间隔的屏幕截图。

请注意如何从函数中删除

newPage
使调用者能够在
setContent
而不是
goto
上拍摄屏幕截图。

© www.soinside.com 2019 - 2024. All rights reserved.