做者简介 felix 蚂蚁金服·数据体验技术团队html
咱们平常使用浏览器的步骤为:启动浏览器、打开一个网页、进行交互。而无头浏览器
指的是咱们使用脚原本执行以上过程的浏览器,能模拟真实的浏览器使用场景。前端
有了无头浏览器,咱们就能作包括但不限于如下事情:node
无头浏览器不少,包括但不限于:git
本文主要介绍 Google 提供的无头浏览器(headless Chrome), 他基于 Chrome DevTools protocol 提供了很多高度封装的接口方便咱们控制浏览器。github
为了能使用
async
/await
等新特性,须要使用 v7.6.0 或更高版本的 Node.chrome
// 启动浏览器
const browser = await puppeteer.launch({
// 关闭无头模式,方便咱们看到这个无头浏览器执行的过程
// headless: false,
timeout: 30000, // 默认超时为30秒,设置为0则表示不设置超时
});
// 打开空白页面
const page = await browser.newPage();
// 进行交互
// ...
// 关闭浏览器
// await browser.close();
复制代码
// 设置浏览器视窗
page.setViewport({
width: 1376,
height: 768,
});
复制代码
// 地址栏输入网页地址
await page.goto('https://google.com/', {
// 配置项
// waitUntil: 'networkidle', // 等待网络状态为空闲的时候才继续执行
});
复制代码
打开一个网页,而后截图保存到本地:数组
await page.screenshot({
path: 'path/to/saved.png',
});
复制代码
完整示例代码浏览器
打开一个网页,而后保存 pdf 到本地:bash
await page.pdf({
path: 'path/to/saved.pdf',
format: 'A4', // 保存尺寸
});
复制代码
完整示例代码服务器
要获取打开的网页中的宿主环境,咱们可使用 Page.evaluate
方法:
// 获取视窗信息
const dimensions = await page.evaluate(() => {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
deviceScaleFactor: window.devicePixelRatio
};
});
console.log('视窗信息:', dimensions);
// 获取 html
// 获取上下文句柄
const htmlHandle = await page.$('html');
// 执行计算
const html = await page.evaluate(body => body.outerHTML, htmlHandle);
// 销毁句柄
await htmlHandle.dispose();
console.log('html:', html);
复制代码
Page.$
能够理解为咱们经常使用的 document.querySelector
, 而 Page.$$
则对应 document.querySelectorAll
。
打开谷歌首页,输入关键字,回车进行搜索:
// 地址栏输入网页地址
await page.goto('https://google.com/', {
waitUntil: 'networkidle', // 等待网络状态为空闲的时候才继续执行
});
// 聚焦搜索框
// await page.click('#lst-ib');
await page.focus('#lst-ib');
// 输入搜索关键字
await page.type('辣子鸡', {
delay: 1000, // 控制 keypress 也就是每一个字母输入的间隔
});
// 回车
await page.press('Enter');
复制代码
每个简单的动做链接起来,就是一连串复杂的交互,接下来咱们看两个更具体的示例。
传统的爬虫是基于 HTTP 协议,模拟 UserAgent 发送 http 请求,获取到 html 内容后使用正则解析出须要抓取的内容,这种方式面对服务端渲染直出 html 的网页时很是便捷。
但遇到单页应用(SPA)时,或遇到登陆校验时,这种爬虫就显得比较无力。
而使用无头浏览器,抓取网页时彻底使用了人机交互时的操做,因此页面的初始化彻底能使用宿主浏览器环境渲染完备,再也不须要关心这个单页应用在前端初始化时须要涉及哪些 HTTP 请求。
无头浏览器提供的各类点击、输入等指令,彻底模拟人的点击、输入等指令,也就不再用担忧正则写不出来了啊哈哈哈
固然,有些场景下,使用传统的 HTTP 爬虫(写正则匹配) 仍是比较高效的。
在这里就再也不详细对比这些差别了,如下这个例子仅做为展现模拟一个完整的人机交互:使用移动版饿了么点外卖。
先看下效果:
代码比较长就不全贴了,关键是几行:
const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone6 = devices['iPhone 6'];
console.log('启动浏览器');
const browser = await puppeteer.launch();
console.log('打开页面');
const page = await browser.newPage();
// 模拟移动端设备
await page.emulate(iPhone6);
console.log('地址栏输入网页地址');
await page.goto(url);
console.log('等待页面准备好');
await page.waitForSelector('.search-wrapper .search');
console.log('点击搜索框');
await page.tap('.search-wrapper .search');
await page.type('麦当劳', {
delay: 200, // 每一个字母之间输入的间隔
});
console.log('回车开始搜索');
await page.tap('button');
console.log('等待搜素结果渲染出来');
await page.waitForSelector('[class^="index-container"]');
console.log('找到搜索到的第一家外卖店!');
await page.tap('[class^="index-container"]');
console.log('等待菜单渲染出来');
await page.waitForSelector('[class^="fooddetails-food-panel"]');
console.log('直接选一个菜品吧');
await page.tap('[class^="fooddetails-cart-button"]');
// console.log('===为了看清楚,傲娇地等两秒===');
await page.waitFor(2000);
await page.tap('[class^=submit-btn-submitbutton]');
// 关闭浏览器
await browser.close();
复制代码
关键步骤是:
关键的几个指令:
page.tap
(或 page.click
) 为点击page.waitForSelector
意思是等待指定元素出如今网页中,若是已经出现了,则当即继续执行下去, 后面跟的参数为 selector
选择器,与咱们经常使用的 document.querySelector
接收的参数一致page.waitFor
后面能够传入 selector
选择器、function
函数或 timeout
毫秒时间,如 page.waitFor(2000)
指等待2秒再继续执行,例子中用这个函数暂停操做主要是为了演示以上几个指令均可接受一个 selector
选择器做为参数,这里额外介绍几个方法:
page.$(selector)
与咱们经常使用的 document.querySelector(selector)
一致,返回的是一个 ElementHandle
元素句柄page.$$(selector)
与咱们经常使用的 document.querySelectorAll(selector)
一致,返回的是一个数组在有头浏览器上下文中,咱们选择一个元素的方法是:
const body = document.querySelector('body');
const bodyInnerHTML = body.innerHTML;
console.log('bodyInnerHTML: ', bodyInnerHTML);
复制代码
而在无头浏览器里,咱们首先须要获取一个句柄,经过句柄获取到环境中的信息后,销毁这个句柄。
// 获取 html
// 获取上下文句柄
const bodyHandle = await page.$('body');
// 执行计算
const bodyInnerHTML = await page.evaluate(dom => dom.innerHTML, bodyHandle);
// 销毁句柄
await bodyHandle.dispose();
console.log('bodyInnerHTML:', bodyInnerHTML);
复制代码
除此以外,还可使用 page.$eval
:
const bodyInnerHTML = await page.$eval('body', dom => dom.innerHTML);
console.log('bodyInnerHTML: ', bodyInnerHTML);
复制代码
page.evaluate
意为在浏览器环境执行脚本,可传入第二个参数做为句柄,而 page.$eval
则针对选中的一个 DOM 元素执行操做。
我在 图灵社区 上买了很多电子书,之前支持推送到 mobi
格式到 kindle
或推送 pdf
格式到邮箱进行阅读,不过常常会关闭这些推送渠道,只能留在网页上看书。
对我来讲不是很方便,而这些书籍的在线阅读效果是服务器渲染出来的(带了大量标签,没法简单抽取出好的排版),最好的方式固然是直接在线阅读并保存为 pdf 或图片了。
借助浏览器的无头模式,我写了个简单的下载已购买书籍为 pdf
到本地的脚本,支持批量下载已购买的书籍。
使用方法,传入账号密码和保存路径,如:
$ node ./demo/download-ituring-books.js '用户名' '密码' './books'
复制代码
注意:puppeteer
的 Page.pdf()
目前仅支持在无头模式中使用,因此要想看有头状态的抓取过程的话,执行到 Page.pdf()
这步会先报错:
因此启动这个脚本时,须要保持无头模式:
const browser = await puppeteer.launch({
// 关闭无头模式,方便咱们看到这个无头浏览器执行的过程
// 注意若调用了 Page.pdf 即保存为 pdf,则须要保持为无头模式
// headless: false,
});
复制代码
看下执行效果:
个人书架里有20多本书,下载完后是这样子:
无头浏览器说白了就是能模拟人工在有头浏览器中的各类操做。那天然不少人力活,都能使用无头浏览器来作(好比上面这个下载 pdf 的过程,实际上是人力打开每个文章页面,而后按 ctrl+p
或 command+p
保存到本地的自动化过程)。
那既然用自动化工具能解决的事情,就不该该浪费重复的人力劳动了,除此以外咱们还能够作:
感兴趣的同窗能够关注专栏或者发送简历至'qingsheng.lqs####alibaba-inc.com'.replace('####', '@'),欢迎有志之士加入~