Puppeteer 是一个由 Google 开发的 Node.js 库,提供了一个高级 API 来控制无头浏览器(如 Chrome 或 Chromium),用于执行自动化浏览器任务。它允许开发者在没有用户界面的情况下启动和操控浏览器,或者也可以以有头模式运行(带有用户界面)。Puppeteer 的主要功能包括网页自动化、网页抓取、生成 PDF 和截图等。

经典用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const puppeteer = require('puppeteer');

(async () => {
// 启动浏览器
const browser = await puppeteer.launch();
const page = await browser.newPage();

// 导航到指定页面
await page.goto('https://example.com');

// 截图页面
await page.screenshot({ path: 'example.png' });

// 关闭浏览器
await browser.close();
})();

使用场景

笔者使用 Puppeteer 主要是出于「网页抓取」的需求。Puppeteer 可用于抓取动态内容,相比传统的抓取工具(如 curl 或 requests),它可以解析并抓取依赖 JavaScript 动态生成内容的网页。Puppeteer 在抓取 SPA(单页应用)或其他依赖 JavaScript 渲染的站点时非常有用。

举个例子,我想要获得雪球中 标普 500 LOF 的价格、溢价等数据,需要先访问其 HTML 静态页,首次访问静态页时,会有一些 set-cookie 的响应返回,之后的异步请求中,需要在 cookie 中携带某个 token 才能通过鉴权。

前些天,雪球似乎还接入了某 WAF,首次访问网页时会返回一段复杂的 JS 代码执行而后二次请求,请求头中具有某个字段才能通过 WAF 验证,进而进入雪球的处理步骤。

出于数据获取的需要,逐个研究目标网站逻辑规则是低效的,因为其逻辑规则或外接安全服务随时可能变化。相比之下,「仿生」是更好的思路,无论目标网站逻辑如何,最终都会让用户可阅读,那么模拟用户行为,并将其转化为程序化流程,是一个可面向诸多不同网站的通用解决方案。

就雪球报价数据而言,可以采用模拟浏览器执行取得 cookie + 主动发起请求的方式快速达成目的。

其中 cookie 获取只需寥寥几行代码

1
2
3
4
5
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto("https://xueqiu.com/S/SZ161125");
await sleep(1000);
return page.cookies("https://xueqiu.com");

微服务

作为 NodeJS 模块,Puppeteer 足够强大且易用,但仍有一些不足之处:

  • 目前笔者的微服务架构中,各应用运行在多台物理机中的 Docker 容器中,应用间的运行环境是隔离的;其中有众多「爬虫应用」
  • Puppeteer 依赖 Chrome,需占用 200MB ~ 300MB 的空间;各应用分别安装 Chrome 无疑增加了应用开发的接入成本,及应用构建/运行所需的时间空间(多数应用的镜像本身是低于 100MB 的)

目前笔者的微服务架构中,各应用运行在多台物理机中的 Docker 容器中,应用间的运行环境是隔离的;实现一个中心化应用(Chrome + Puppeteer)并开放 API 供各应用调用,可弥补上述不足。

puppeteer-api

puppeteer-api 集成了 Puppeteer 及其依赖的 Chrome 模块,调用者可通过 API 传递「动态代码」。应用开发人员可获得直接接入 Puppeteer 相似的体验,而无需额外安装任何模块。

使用 Docker 启动

1
2
3
4
5
6
docker pull fangqk1991/puppeteer-api

docker run -d --restart=unless-stopped \
--name puppeteer-api \
-p 6789:6789 \
fangqk1991/puppeteer-api

访问 Swagger 文档

执行示例代码

1
curl -X POST 'http://localhost:6789/api/v1/puppeteer/example'

执行自定义代码

1
2
3
4
curl -X 'POST' \  
'http://localhost:6789/api/v1/puppeteer/execute' \ -H 'Content-Type: text/plain' \ -d 'const page = await browser.newPage();await page.goto("https://xueqiu.com/S/SZ161125");
await sleep(1000);
return page.cookies("https://xueqiu.com");'