本文通过 puppeteer 入手用 nodejs 实现后台模拟浏览器爬取异步加载的网页上的数据,以网易云mlog的视频做一个例子。
毕竟是模拟浏览器,不像 python 可以实现异步百万并发,模拟浏览器是没什么惊人效率的,了解一下即可。
项目地址:puppeteer / puppeteer
API 文档:英文文档 | 中文文档
原理:模拟浏览器,以实现截图、点击等浏览器事件,从而有了等待异步加载的能力。
安装的时候需要注意,我们安装 puppeteer-core 核心功能即可,无需安装 puppeteer ,因为 puppeteer 附带一个 200 M 的浏览器,我们本地有 chrome 的情况不需要安装 puppeteer 附带的浏览器。
yarn add puppeteer-core
看官方文档提供的一个最简单的截屏实例:
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();
})();
相关操作是非常简单的,这部分不再做详述。
选取网易云 mlog 作为实例的原因:
像网易云这种前端都是带 params, encSecKey 混淆的异步加载页面,不去仔细研究混淆方法是不行的,虽然市面上大家对网易云这两个参数研究的很透彻了,但是 mlog 是网易云的新产品(仅面向手机端,类似抖音快手的小视频模式,pc 端没有),所以其加密参数需要哪些也是要很多精力研究的,这正是我们选择这个实例的原因。
注:如果不懂网易云前端混淆的也不要紧,总而言之,异步加载的页面意味着普通爬虫获取不到全部节点,直接请求后台 api 接口需要被混淆的参数,这就意味着在复杂混淆的情况下你要 hook 请求去分析被 webpack 打包的 js 源码,不值得浪费太多精力。
const puppeteer = require("puppeteer-core");
const fs = require("fs");
const argv = process.argv.splice(2);
const url = argv[0];
const executablePath = argv[1];
if (!url || !executablePath) {
process.exit(1);
}
(async () => {
const browser = await puppeteer.launch({ executablePath });
const page = await browser.newPage();
page.waitForSelector("video").then((e) => {
const data = e.getProperty("src").then((a) => {
a.jsonValue().then((b) => {
fs.writeFile("out.json", JSON.stringify({ src: b }), (err) => {});
});
});
})
.catch((err) => {
fs.writeFile("out.json", JSON.stringify({ src: "null" }), (err) => {});
});
await page.goto(url);
await browser.close();
})();
我们分析一下:
const puppeteer = require("puppeteer-core");
const fs = require("fs");
const argv = process.argv.splice(2);
const url = argv[0];
const executablePath = argv[1];
if (!url || !executablePath) {
process.exit(1);
}
↑ 导入 puppeteer-core ,并取命令行的第二个参数作为视频 url 地址,第三个参数作为 chrome 浏览器在本机的位置。如果缺少参数则退出。
const browser = await puppeteer.launch({ executablePath });
const page = await browser.newPage();
↑ 开一个无头在后台的模拟浏览器,并打开一个新页面
page.waitForSelector("video").then((e) => {
const data = e.getProperty("src").then((a) => {
a.jsonValue().then((b) => {
fs.writeFile("out.json", JSON.stringify({ src: b }), (err) => {});
});
});
})
.catch((err) => {
fs.writeFile("out.json", JSON.stringify({ src: "null" }), (err) => {});
});
↑ 等待视频标签 video 加载完成,执行回调,得到一个 ElementHandle 节点类,这个类是 puppeteer 包装的,利用其 getProperty()
方法可以取到该节点某个属性值的 Promise 包装。
继续回调,该 Promise 可以回调拿到一个 JSHandle 类,该类包括了刚刚我们指定的属性值信息,用 JSHandle 类的 jsonValue()
方法可以拿到一个包裹 Object 的 Promise 包装,再回调直接就拿到此 Object 值。
最后我们写入 json 文件,不存在会超时报错, catch 到写 null 。
await page.goto(url);
await browser.close();
↑ 刚刚我们定义完了要做什么,现在开始访问页面,执行完毕后关闭浏览器。
注:以上所有 API 和 类 的信息都可以在官方文档内找到。
仔细品味一下,这么复杂的回调其实不是给人用作爬虫的,而是为了方便截图和做调试的,毕竟是 chrome 专属,目前该库在网页截屏上用的比较广泛,我们可以通过制定浏览器大小:
await page.setViewport({
width: 414,
height: 736,
deviceScaleFactor: 3
})
↑ 来生成指定大小的页面,其中 deviceScaleFactor
是分辨率倍数,越大越清晰,最后生成的图片越大。
await page.screenshot({
path: `example.png`,
fullPage: true
})
↑ 再获得一个全页面的截图
注:有些图片可能是懒加载,对于加载时间很久的或者懒加载的还可以使用滚动页面和人为延时等对策。
更多方法说明请看官方文档。