logo科技微讯

在云函数使用 puppeteer 爬取网页内容,如何更快一点?

作者:科技微讯
日期:2021-06-10
📜 文章

云函数可以用 puppeteer 吗?可以。这篇文章以运行在 Node.js 环境下的云函数为例子。

腾讯云函数的运行环境内置了 puppeteer 库,开发者可以直接使用,不需要额外安装。对于没有内置 puppeteer 的云函数环境,可以使用 chrome-aws-lambda,虽然名称带有 aws lambda 字样,但它不仅可以用在 aws lambda,还可以用在 google cloud function、vercel function 中。

在 Vercel 上使用 puppeteer

首先说一下怎样在 Vercel 上通过 chrome-aws-lambda 使用 puppeteer。搜索了一下,在 GitHub 上找到一个 issue,网友们很清晰地分享了使用方法。

其实只需要注意一点,chrome-aws-lambda 不能在本地电脑运行,所以你需要在代码中判断是否是本地运行,如果是,则调用本地的浏览器,如果在云函数调用则使用 chrome-aws-lambda。

const chrome = require("chrome-aws-lambda");
const puppeteer = require("puppeteer-core");
const fs = require("fs");
async function app(url) {
  //通过 process.env.FUNCTION_NAME 判断你是在本地运行还是在云函数运行
  //你需要在云函数设置一个名为 FUNCTION_NAME 的环境变量,FUNCTION_NAME 可以修改为其他名字,只要有这个环境变量即可
  //如果你在本地运行,executablePath 就是你电脑安装的 Chrome 浏览器
  //puppeteer 可以接收的所有 options:https://github.com/puppeteer/puppeteer/blob/v10.0.0/docs/api.md#puppeteerlaunchoptions
  const options = process.env.FUNCTION_NAME
    ? {
        args: chrome.args,
        executablePath: await chrome.executablePath,
        headless: chrome.headless,
      }
    : {
        args: [],
        executablePath:
          process.platform === "win32"
            ? "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"
            : process.platform === "linux"
            ? "/usr/bin/google-chrome"
            : "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
      };

  const browser = await puppeteer.launch(options);
  const page = await browser.newPage();
  await page.setViewport({ width: 2000, height: 1000 });
  await page.goto(url, { waitUntil: "networkidle0" });
  const buffer = await page.screenshot({ type: "png" });
  fs.writeFileSync("./screenshot.png", buffer);
}
app("https://kejiweixun.com");

遇到的问题

使用 puppeteer 打开一些比较复杂的网页时,耗时可能比较长,对于按量计费的云函数,运行时间越长费用越高,而且云函数都有运行时间限制,比如 Vercel 免费账户有 10s 的限制,付费账户是 60s,微信小程序云开发的云函数也是 60s,而腾讯云函数则有 900s 的限制。

最近尝试在 Vercel 上使用 puppeteer,我是免费账号,发现很容易超时,不过可以通过一些技巧降低运行时间。

解决方法

禁止请求不需要的内容

要明确你使用 puppeteer 的目的是什么,如果只是爬取网页上的文字元素,那你可以屏蔽所有 image、css、font 请求,关键代码如下:

//需要先设置 setRequestInterception 为 true
//因为默认情况下 request 只可读
await page.setRequestInterception(true);
page.on("request", (req) => {
  if (
    req.resourceType() == "stylesheet" ||
    req.resourceType() == "font" ||
    req.resourceType() == "image"
  ) {
    req.abort();
  } else {
    req.continue();
  }
});

这样做可以明显降低 puppeteer 的运行时间,我的云函数运行时间从 9-10s 一下子降到 5-6s。当然这样做只适用于你仅仅需要网页的文字元素而无所谓图片以及页面布局的情况。

可以不关闭浏览器吗

浏览器关闭后再打开往往耗时几百毫秒,可以像处理数据库连接那样,把浏览器缓存起来,当云函数被复用时可以复用浏览器对象,节省关闭、重新打开浏览器所消耗的时间。这其中有很多需要注意的地方,比如不关闭浏览器如何避免云函数超时呢?详情,可以看我之前写的《腾讯云函数连接 mongoDB 的正确方法:保持数据库连接而不超时》。

此外,用来缓存浏览器对象的变量,即使浏览器被关闭了,这个变量也会保持 truthy,因此需要通过监控浏览器的 'disconnected' 事件把这个变量重新变成 falsy。最后提醒一下:如果不 close 浏览器,那记得 close page,否则 page 会越积越多,最后会导致内存超额出错。

const chrome = require("chrome-aws-lambda");
const puppeteer = require("puppeteer-core");
let browser = null;
const openBrowser = async () => {
  if (!browser) {
    browser = await puppeteer.launch(
      process.env.FUNCTION_NAME
        ? {
            args: chrome.args,
            executablePath: await chrome.executablePath,
            headless: chrome.headless,
          }
        : {
            args: [],
            executablePath:
              "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
          }
    );
    browser.on("disconnected", () => {
      console.log("浏览器断开连接");
      //断开后设为 falsy 值
      browser = null;
    });
  } else {
    console.log("浏览器已经打开,不需要重新打开");
  }
  return browser;
};
module.exports = openBrowser;

只等待需要的内容

使用 puppeteer 就像我们使用 Chrome 等浏览器,通常来说我们打开一个网页,不会等所有内容都加载完成才开始使用这个网页,而是当我们需要的页面元素出现在我们眼前时,我们就会马上拿起鼠标或按下键盘进行操作。

人的反应比不上程序,通过 puppeteer 的 waitForSelector,我们可以缩短“马上”的时间。比如下面的代码:打开一个 url,等待这个页面的提交按钮的出现,出现后马上点击。

await page.goto(url);
await page.waitForSelector(".submit-button", { visible: true });
//马上点击 submit-button

再比如等待页面出现某些文字内容,就马上采取行动:

//等待,直到 waitForFunction 里面的值为 true
await page.waitForFunction(
  'document.querySelector("body").innerText.includes("some text to wait")'
);
//接下来马上采取行动

另一个控制等待时间的方法是,尝试给 waitUntil 设置不同的值。

如果你需要等待网页加载完毕,但是不确定一个页面的“加载完毕”是什么意思,可以尝试把 waitUntil 设置为 networkidle2、domcontentloaded、load 等值,观察是否能得到你要的东西。

  • domcontentloaded:html
  • load:html、js
  • networkidle0、networkidle2:html、js、xhr
await page.goto(url, { waitUntil: "domcontentloaded" });

如果你只是想获得某些 xhr 请求的数据,那当这些 xhr 返回数据时,就可以关闭浏览器了:

//注意 page.on 要写在 page.goto 之前
page.on("response", async (res) => {
  const url = await res.url();
  if (url.match(/api.kejiweixun.com/gi)) {
    const re = await res.json();
    console.log(re);
    browser.close();
  }
});
await page.goto(url);

同时打开多个网页

在 puppeteer 中,调用 browser.newPage() 会打开一个新的网页标签,这里的 page 其实就是 tab。有人发现如果在同一个浏览器窗口同时打开 10 个标签,即 browser.newPage() 十次,每个标签访问同一个页面,结果网页访问速度会随着打开标签数量的增加而下降。

chrome-aws-lambda 的开发者发现,可以打开 10 个浏览器窗口,而且都是隐私模式,再分别访问这个网页,则不存在这个问题。

const promises = Array(10)
  .fill(null)
  .map(async () => {
    const context = await browser.createIncognitoBrowserContext();
    const page = await context.newPage();
    await page.goto("https://www.booking.com");
  });
await Promise.all(promises);

设置合理的 timeout

有时候你等待的东西可能永远不会出现,比如 waitForSelector 中的 selector 可能突然没有了(比如网页改版),这可能会导致云函数超时出错,所以使用 puppeteer 的 api 时需要设置合理的 timeout 时长。

//比如根据你的经验,打开 xx 网页正常来说不会超过 3s
//如果超过 3s,大概率是哪里出问题了,那可以设置 timeout 为 3s
await page.goto(url, { timeout: 3000 });

总结

以上方法可以结合起来用,另外 chrome-aws-lambda 还可以搭配 playwright 使用。


相关阅读:

donation赞赏
thumbsup0
thumbsdown0
暂无评论