详解Puppeteer 入门教程
1、Puppeteer简介
Puppeteer是一个node库,他提供了一组用来操纵Chrome的API,通俗来说就是一个headlesschrome浏览器(当然你也可以配置成有UI的,默认是没有的)。既然是浏览器,那么我们手工可以在浏览器上做的事情Puppeteer都能胜任,另外,Puppeteer翻译成中文是”木偶”意思,所以听名字就知道,操纵起来很方便,你可以很方便的操纵她去实现:
1)生成网页截图或者PDF
2)高级爬虫,可以爬取大量异步渲染内容的网页
3)模拟键盘输入、表单自动提交、登录网页等,实现UI自动化测试
4)捕获站点的时间线,以便追踪你的网站,帮助分析网站性能问题
如果你用过PhantomJS的话,你会发现她们有点类似,但Puppeteer是Chrome官方团队进行维护的,用俗话说就是”有娘家的人“,前景更好。
2、运行环境
查看Puppeteer的官方API你会发现满屏的async,await之类,这些都是ES7的规范,所以你需要:
- Nodejs的版本不能低于v7.6.0,需要支持async,await.
- 需要最新的chromedriver,这个你在通过npm安装Puppeteer的时候系统会自动下载的
npminstallpuppeteer--save
3、基本用法
先开看看官方的入门的DEMO
constpuppeteer=require('puppeteer'); (async()=>{ constbrowser=awaitpuppeteer.launch(); constpage=awaitbrowser.newPage(); awaitpage.goto('https://example.com'); awaitpage.screenshot({path:'example.png'}); awaitbrowser.close(); })();
上面这段代码就实现了网页截图,先大概解读一下上面几行代码:
- 先通过puppeteer.launch()创建一个浏览器实例Browser对象
- 然后通过Browser对象创建页面Page对象
- 然后page.goto()跳转到指定的页面
- 调用page.screenshot()对页面进行截图
- 关闭浏览器
是不是觉得好简单?反正我是觉得比PhantomJS简单,至于跟selenium-webdriver比起来,那更不用说了。下面就介绍一下puppeteer的常用的几个API。
3.1puppeteer.launch(options)
使用puppeteer.launch()运行puppeteer,它会return一个promise,使用then方法获取browser实例,当然高版本的的nodejs已经支持await特性了,所以上面的例子使用await关键字,这一点需要特殊说明一下,Puppeteer几乎所有的操作都是异步的,为了使用大量的then使得代码的可读性降低,本文所有demo代码都是用async,await方式实现。这个也是Puppeteer官方推荐的写法。对async/await一脸懵逼的同学狠狠的戳这里
options参数详解
参数名称
参数类型
参数说明
ignoreHTTPSErrors
boolean
在请求的过程中是否忽略Https报错信息,默认为false
headless
boolean
是否以”无头”的模式运行chrome,也就是不显示UI,默认为true
executablePath
string
可执行文件的路劲,Puppeteer默认是使用它自带的chromewebdriver,如果你想指定一个自己的webdriver路径,可以通过这个参数设置
slowMo
number
使Puppeteer操作减速,单位是毫秒。如果你想看看Puppeteer的整个工作过程,这个参数将非常有用。
args
Array(String)
传递给chrome实例的其他参数,比如你可以使用”–ash-host-window-bounds=1024x768”来设置浏览器窗口大小。更多参数参数列表可以参考这里
handleSIGINT
boolean
是否允许通过进程信号控制chrome进程,也就是说是否可以使用CTRL+C关闭并退出浏览器.
timeout
number
等待Chrome实例启动的最长时间。默认为30000(30秒)。如果传入0的话则不限制时间
dumpio
boolean
是否将浏览器进程stdout和stderr导入到process.stdout和process.stderr中。默认为false。
userDataDir
string
设置用户数据目录,默认linux是在~/.config目录,window默认在C:\Users{USER}\AppData\Local\Google\Chrome\UserData,其中{USER}代表当前登录的用户名
env
Object
指定对Chromium可见的环境变量。默认为process.env。
devtools
boolean
是否为每个选项卡自动打开DevTools面板,这个选项只有当headless设置为false的时候有效
3.2Browser对象
当Puppeteer连接到一个Chrome实例的时候就会创建一个Browser对象,有以下两种方式:
Puppeteer.launch和Puppeteer.connect.
下面这个DEMO实现断开连接之后重新连接浏览器实例
constpuppeteer=require('puppeteer'); puppeteer.launch().then(asyncbrowser=>{ //保存Endpoint,这样就可以重新连接Chromium constbrowserWSEndpoint=browser.wsEndpoint(); //从Chromium断开连接 browser.disconnect(); //使用endpoint重新和Chromiunm建立连接 constbrowser2=awaitpuppeteer.connect({browserWSEndpoint}); //CloseChromium awaitbrowser2.close(); });
Browser对象API
好了,Puppeteer的API就不一一介绍了,官方提供的详细的API,戳这里
4、Puppeteer实战
了解API之后我们就可以来一些实战了,在此之前,我们先了解一下Puppeteer的设计原理,简单来说Puppeteer跟webdriver以及PhantomJS最大的的不同就是它是站在用户浏览的角度,而webdriver和PhantomJS最初设计就是用来做自动化测试的,所以它是站在机器浏览的角度来设计的,所以它们使用的是不同的设计哲学。举个栗子,加入我需要打开京东的首页并进行一次产品搜索,分别看看使用Puppeteer和webdriver的实现流程:
Puppeteer的实现流程:
- 打开京东首页
- 将光标focus到搜索输入框
- 键盘点击输入文字
- 点击搜索按钮
webdriver的实现流程:
- 打开京东首页
- 找到输入框的input元素
- 设置input的值为要搜索文字
- 触发搜索按钮的单机事件
个人感觉Puppeteer设计哲学更符合任何的操作习惯,更自然一些。
下面我们就用一个简单的需求实现来进行Puppeteer的入门学习。这个简单的需求就是:
在京东商城抓取10个手机商品,并把商品的详情页截图。
首先我们来梳理一下操作流程
- 打开京东首页
- 输入“手机”关键字并搜索
- 获取前10个商品的A标签,并获取href属性值,获取商品详情链接
- 分别打开10个商品的详情页,截取网页图片
要实现上面的功能需要用到查找元素,获取属性,键盘事件等,那接下来我们就一个一个的讲解一下。
4.1获取元素
Page对象提供了2个API来获取页面元素
(1).Page.$(selector)获取单个元素,底层是调用的是document.querySelector(),所以选择器的selector格式遵循css选择器规范
letinputElement=awaitpage.$("#search",input=>input); //下面写法等价 letinputElement=awaitpage.$('#search');
(2).Page.$$(selector)获取一组元素,底层调用的是document.querySelectorAll().返回Promise(Array(ElemetHandle))元素数组.
constlinks=awaitpage.$$("a"); //下面写法等价 constlinks=awaitpage.$$("a",links=>links);
最终返回的都是ElemetHandle对象
4.2获取元素属性
Puppeteer获取元素属性跟我们平时写前段的js的逻辑有点不一样,按照通常的逻辑,应该是现获取元素,然后在获取元素的属性。但是上面我们知道获取元素的API最终返回的都是ElemetHandle对象,而你去查看ElemetHandle的API你会发现,它并没有获取元素属性的API.
事实上Puppeteer专门提供了一套获取属性的API,Page.$eval()和Page.$$eval()
(1).Page.$$eval(selector,pageFunction[,…args]),获取单个元素的属性,这里的选择器selector跟上面Page.$(selector)是一样的。
constvalue=awaitpage.$eval('input[name=search]',input=>input.value); consthref=awaitpage.$eval('#a",ele=>ele.href); constcontent=awaitpage.$eval('.content',ele=>ele.outerHTML);
4.3执行自定义的JS脚本
Puppeteer的Page对象提供了一系列evaluate方法,你可以通过他们来执行一些自定义的js代码,主要提供了下面三个API
(1).page.evaluate(pageFunction,…args)返回一个可序列化的普通对象,pageFunction表示要在页面执行的函数,args表示传入给pageFunction的参数,下面的pageFunction和args表示同样的意思。
constresult=awaitpage.evaluate(()=>{ returnPromise.resolve(8*7); }); console.log(result);//prints"56"
这个方法很有用,比如我们在获取页面的截图的时候,默认是只截图当前浏览器窗口的尺寸大小,默认值是800x600,那如果我们需要获取整个网页的完整截图是没办法办到的。Page.screenshot()方法提供了可以设置截图区域大小的参数,那么我们只要在页面加载完了之后获取页面的宽度和高度就可以解决这个问题了。
(async()=>{ constbrowser=awaitpuppeteer.launch({headless:true}); constpage=awaitbrowser.newPage(); awaitpage.goto('https://jr.dayi35.com'); awaitpage.setViewport({width:1920,height:1080}); constdocumentSize=awaitpage.evaluate(()=>{ return{ width:document.documentElement.clientWidth, height:document.body.clientHeight, } }) awaitpage.screenshot({path:"example.png",clip:{x:0,y:0,width:1920,height:documentSize.height}}); awaitbrowser.close(); })();
(2).Page.evaluateHandle(pageFunction,…args)在Page上下文执行一个pageFunction,返回JSHandle实体
constaWindowHandle=awaitpage.evaluateHandle(()=>Promise.resolve(window)); aWindowHandle;//Handleforthewindowobject. constaHandle=awaitpage.evaluateHandle('document');//Handleforthe'document'.
从上面的代码可以看出,page.evaluateHandle()方法也是通过Promise.resolve方法直接把Promise的最终处理结果返回,只不过把最后返回的对象封装成了JSHandle对象。本质上跟evaluate没有什么区别。
下面这段代码实现获取页面的动态(包括js动态插入的元素)HTML代码.
constaHandle=awaitpage.evaluateHandle(()=>document.body); constresultHandle=awaitpage.evaluateHandle(body=>body.innerHTML,aHandle); console.log(awaitresultHandle.jsonValue()); awaitresultHandle.dispose();
(3).page.evaluateOnNewDocument(pageFunction,…args),在文档页面载入前调用pageFunction,如果页面中有iframe或者frame,则函数调用的上下文环境将变成子页面的,即iframe或者frame,由于是在页面加载前调用,这个函数一般是用来初始化javascript环境的,比如重置或者初始化一些全局变量。
4.4Page.exposeFunction
除此上面三个API之外,还有一类似的非常有用的API,那就是Page.exposeFunction,这个API用来在页面注册全局函数,非常有用:
因为有时候需要在页面处理一些操作的时候需要用到一些函数,虽然可以通过Page.evaluate()API在页面定义函数,比如:
constdocSize=awaitpage.evaluate(()=>{ functiongetPageSize(){ return{ width:document.documentElement.clientWidth, height:document.body.clientHeight, } } returngetPageSize(); });
但是这样的函数不是全局的,需要在每个evaluate中去重新定义,无法做到代码复用,在一个就是nodejs有很多工具包可以很轻松的实现很复杂的功能比如要实现md5加密函数,这个用纯js去实现就不太方便了,而用nodejs却是几行代码的事情。
下面代码实现给Page上下文的window对象添加md5函数:
constpuppeteer=require('puppeteer'); constcrypto=require('crypto'); puppeteer.launch().then(asyncbrowser=>{ constpage=awaitbrowser.newPage(); page.on('console',msg=>console.log(msg.text)); awaitpage.exposeFunction('md5',text=> crypto.createHash('md5').update(text).digest('hex') ); awaitpage.evaluate(async()=>{ //usewindow.md5tocomputehashes constmyString='PUPPETEER'; constmyHash=awaitwindow.md5(myString); console.log(`md5of${myString}is${myHash}`); }); awaitbrowser.close(); });
可以看出,Page.exposeFunctionAPI使用起来是很方便的,也非常有用,在比如给window对象注册readfile全局函数:
constpuppeteer=require('puppeteer'); constfs=require('fs'); puppeteer.launch().then(asyncbrowser=>{ constpage=awaitbrowser.newPage(); page.on('console',msg=>console.log(msg.text)); awaitpage.exposeFunction('readfile',asyncfilePath=>{ returnnewPromise((resolve,reject)=>{ fs.readFile(filePath,'utf8',(err,text)=>{ if(err) reject(err); else resolve(text); }); }); }); awaitpage.evaluate(async()=>{ //usewindow.readfiletoreadcontentsofafile constcontent=awaitwindow.readfile('/etc/hosts'); console.log(content); }); awaitbrowser.close(); });
5、Page.emulate修改模拟器(客户端)运行配置
Puppeteer提供了一些API供我们修改浏览器终端的配置
- Page.setViewport()修改浏览器视窗大小
- Page.setUserAgent()设置浏览器的UserAgent信息
- Page.emulateMedia()更改页面的CSS媒体类型,用于进行模拟媒体仿真。可选值为“screen”,“print”,“null”,如果设置为null则表示禁用媒体仿真。
- Page.emulate()模拟设备,参数设备对象,比如iPhone,Mac,Android等
page.setViewport({width:1920,height:1080});//设置视窗大小为1920x1080 page.setUserAgent('Mozilla/5.0(X11;Linuxx86_64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/60.0.3112.90Safari/537.36'); page.emulateMedia('print');//设置打印机媒体样式
除此之外我们还可以模拟非PC机设备,比如下面这段代码模拟iPhone6访问google:
constpuppeteer=require('puppeteer'); constdevices=require('puppeteer/DeviceDescriptors'); constiPhone=devices['iPhone6']; puppeteer.launch().then(asyncbrowser=>{ constpage=awaitbrowser.newPage(); awaitpage.emulate(iPhone); awaitpage.goto('https://www.google.com'); //otheractions... awaitbrowser.close(); });
Puppeteer支持很多设备模拟仿真,比如Galaxy,iPhone,IPad等,想要知道详细设备支持,请戳这里DeviceDescriptors.js.
6、键盘和鼠标
键盘和鼠标的API比较简单,键盘的几个API如下:
- keyboard.down(key[,options])触发keydown事件
- keyboard.press(key[,options])按下某个键,key表示键的名称,比如‘ArrowLeft'向左键,详细的键名映射请戳这里
- keyboard.sendCharacter(char)输入一个字符
- keyboard.type(text,options)输入一个字符串
- keyboard.up(key)触发keyup事件
page.keyboard.press("Shift");//按下Shift键 page.keyboard.sendCharacter('嗨'); page.keyboard.type('Hello');//一次输入完成 page.keyboard.type('World',{delay:100});//像用户一样慢慢输入
鼠标操作:
mouse.click(x,y,[options])移动鼠标指针到指定的位置,然后按下鼠标,这个其实mouse.move和mouse.down或mouse.up的快捷操作
mouse.down([options])触发mousedown事件,options可配置:
- options.button按下了哪个键,可选值为[left,right,middle],默认是left,表示鼠标左键
- options.clickCount按下的次数,单击,双击或者其他次数
- delay按键延时时间
mouse.move(x,y,[options])移动鼠标到指定位置,options.steps表示移动的步长
mouse.up([options])触发mouseup事件
7、另外几个有用的API
Puppeteer还提供几个非常有用的API,比如:
7.1Page.waitFor系列API
- page.waitFor(selectorOrFunctionOrTimeout[,options[,…args]])下面三个的综合API
- page.waitForFunction(pageFunction[,options[,…args]])等待pageFunction执行完成之后
- page.waitForNavigation(options)等待页面基本元素加载完之后,比如同步的HTML,CSS,JS等代码
- page.waitForSelector(selector[,options])等待某个选择器的元素加载之后,这个元素可以是异步加载的,这个API非常有用,你懂的。
比如我想获取某个通过js异步加载的元素,那么直接获取肯定是获取不到的。这个时候就可以使用page.waitForSelector来解决:
awaitpage.waitForSelector('.gl-item');//等待元素加载之后,否则获取不到异步加载的元素 constlinks=awaitpage.$$eval('.gl-item>.gl-i-wrap>.p-img>a',links=>{ returnlinks.map(a=>{ return{ href:a.href.trim(), name:a.title } }); });
其实上面的代码就可以解决我们最上面的需求,抓取京东的产品,因为是异步加载的,所以使用这种方式。
7.2page.getMetrics()
通过page.getMetrics()可以得到一些页面性能数据,捕获网站的时间线跟踪,以帮助诊断性能问题。
- Timestamp度量标准采样的时间戳
- Documents页面文档数
- Frames页面frame数
- JSEventListeners页面内事件监听器数
- Nodes页面DOM节点数
- LayoutCount页面布局总数
- RecalcStyleCount样式重算数
- LayoutDuration所有页面布局的合并持续时间
- RecalcStyleDuration所有页面样式重新计算的组合持续时间。
- ScriptDuration所有脚本执行的持续时间
- TaskDuration所有浏览器任务时长
- JSHeapUsedSizeJavaScript占用堆大小
- JSHeapTotalSizeJavaScript堆总量
8、总结和源码
本文通过一个实际需求来学习了Puppeteer的一些基本的常用的API,API的版本是v0.13.0-alpha.最新邦本的API请参考Puppeteer官方API.
总的来说,Puppeteer真是一款不错的headless工具,操作简单,功能强大。用来做UI自动化测试,和一些小工具都是很不错的。
下面贴上我们开始的需求实现源码,仅供参考:
//延时函数 functionsleep(delay){ returnnewPromise((resolve,reject)=>{ setTimeout(()=>{ try{ resolve(1) }catch(e){ reject(0) } },delay) }) } constpuppeteer=require('puppeteer'); puppeteer.launch({ ignoreHTTPSErrors:true, headless:false,slowMo:250, timeout:0}).then(asyncbrowser=>{ letpage=awaitbrowser.newPage(); awaitpage.setJavaScriptEnabled(true); awaitpage.goto("https://www.jd.com/"); constsearchInput=awaitpage.$("#key"); awaitsearchInput.focus();//定位到搜索框 awaitpage.keyboard.type("手机"); constsearchBtn=awaitpage.$(".button"); awaitsearchBtn.click(); awaitpage.waitForSelector('.gl-item');//等待元素加载之后,否则获取不异步加载的元素 constlinks=awaitpage.$$eval('.gl-item>.gl-i-wrap>.p-img>a',links=>{ returnlinks.map(a=>{ return{ href:a.href.trim(), title:a.title } }); }); page.close(); constaTags=links.splice(0,10); for(vari=1;i{ letscrollTop=document.scrollingElement.scrollTop; document.scrollingElement.scrollTop=scrollTop+scrollStep; returndocument.body.clientHeight>scrollTop+1080?true:false },scrollStep); awaitsleep(100); } awaitpage.waitForSelector("#footer-2014",{timeout:0});//判断是否到达底部了 letfilename="images/items-"+i+".png"; //这里有个Puppeteer的bug一直没有解决,发现截图的高度最大只能是16384px,超出部分被截掉了。 awaitpage.screenshot({path:filename,fullPage:true}); page.close(); } browser.close(); });
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。