内容简介:系统环境的话在工作时使用MacOS,部署到服务器上的是Centos 7. 在如果
Puppeteer
是一个 Node
库,它提供了一个高级API来通过DevTools协议控制 Chromium
。 在谷歌推出这款 headless
浏览器后, Selenium
直接被我抛弃了,因为 Puppeteer
对于 Nodejs
开发者来说简直太友好了,(正常情况下)只需要 npm i puppeteer
,即可完成安装,而不需要安装其他的依赖库( 当初太年轻o(╥﹏╥)o,其实并不简单 )。
系统环境的话在工作时使用MacOS,部署到服务器上的是Centos 7. 在 MacOS
上确实简单,只需要 npm i puppeteer
就行。安装不了有下列几条解决办法:
# 1. 设置环境变量跳过下载 Chromium(2018-09-03已失效) set PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 # 2. 只下载模块而不build,但chromium需要自行下载(2018-09-03有效) npm i --save puppeteer --ignore-scripts # 3. Puppeteer从v1.7.0开始额外提供一个puppeteer-core的库,它只包含Puppeteer的核心库,默认不下载chromium npm i puppeteer-core # 如果连puppeteer都安装不了,建议使用淘宝镜像 npm config set registry="https://registry.npm.taobao.org" 复制代码
如果 Chromium
是自行下载的,则启动 headless
浏览器时需增加如下配置项
this.browser = await puppeteer.launch({ // MacOS应该在"xxx/Chromium.app/Contents/MacOS/Chromium",Linux应该"/usr/bin/chromium-browser" executablePath: "Chromium的安装路径", // 去沙盒 args: ['--no-sandbox', '--disable-dev-shm-usage'], }); 复制代码
二、技巧
懒加载截图
在截图或者爬虫时,常常遇到一些页面采用懒加载的方式展示数据,首屏是不会展示全部的信息给我们。 针对懒加载,采用滚动到底的方式来破解。 啥?懒加载没有底,尝试直接调他们的接口吧,或者还有其他高明的方式欢迎指出
page.evaluate(pageFunction, ...args) : 该函数能让我们使用内置的DOM选择器
这里要特别注意下 pageFunction
的传参方式为:
const result = await page.evaluate(param1, param2, param3 => { return Promise.resolve(8 + param1 + param2 + param3); }, param1, param2, param3); // 也可以传一个字符串: console.log(await page.evaluate('1 + 2')); // 输出 "3" const x = 10; console.log(await page.evaluate(`1 + ${x}`)); // 输出 "11" 复制代码
代码:以简书的懒加载为例
/** * 懒加载页面自动滚动 */ const path = require('path'); const puppeteer = require('puppeteer-core'); const log = console.log; (async () => { const browser = await puppeteer.launch({ // executablePath: path.join(__dirname, './chromium/Chromium.app/Contents/MacOS/Chromium'), // 关闭headless模式, 会打开浏览器 headless: false, args: ['--no-sandbox', '--disable-dev-shm-usage'], }); const page = await browser.newPage(); await page.goto('https://www.jianshu.com/u/40909ea33e50'); await autoScroll(page); // fullPage截图 await page.screenshot({ path: 'auto_scroll.png', type: 'png', fullPage: true, }); await browser.close(); })(); async function autoScroll(page) { log('[AutoScroll begin]'); await page.evaluate(async () => { await new Promise((resolve, reject) => { // 页面的当前高度 let totalHeight = 0; // 每次向下滚动的距离 let distance = 100; // 通过setInterval循环执行 let timer = setInterval(() => { let scrollHeight = document.body.scrollHeight; // 执行滚动操作 window.scrollBy(0, distance); // 如果滚动的距离大于当前元素高度则停止执行 totalHeight += distance; if (totalHeight >= scrollHeight) { clearInterval(timer); resolve(); } }, 100); }); }); log('[AutoScroll done]'); // 完成懒加载后可以完整截图或者爬取数据等操作 // do what you like ... } 复制代码
元素精确截图
精确截图,顾名思义是将元素在页面上所占据的区域 抠
下来。 那么换成 Puppeteer
的方式来处理,是利用 screenshot
的 clip
参数,根据元素相对视窗的坐标( x、y
)及元素的款宽高( width、height
)定位截图。当然了,元素选择器必须要找准,否则再怎么样也无法精确截图
- page.screenshot参数 clip
-
element.getBoundingClientRect()
: 通过这个方法可以获取到元素在视窗内的相对位置(返回对象中包括left、top、width、height
),相关知识点可谷歌了解下 -
$eval
: 此方法在页面内执行document.querySelector
,然后把匹配到的元素作为第一个参数传给pageFunction
const path = require('path'); const puppeteer = require('puppeteer-core'); const log = console.log; (async () => { const browser = await puppeteer.launch({ // executablePath: path.join(__dirname, './chromium/Chromium.app/Contents/MacOS/Chromium'), // 关闭headless模式, 会打开浏览器 headless: false, args: ['--no-sandbox', '--disable-dev-shm-usage'], }); const page = await browser.newPage(); await page.goto('https://www.jianshu.com/'); const pos = await getElementBounding(page, '.board'); // clip截图 await page.screenshot({ path: 'element_bounding.png', type: 'png', clip: { x: pos.left, y: pos.top, width: pos.width, height: pos.height } }); await browser.close(); })(); async function getElementBounding(page, element) { log('[GetElementBounding]: ', element); const pos = await page.$eval(element, e => { // 相当于在evaluate的pageFunction内执行 // document.querySelector(element).getBoundingClientRect() const {left, top, width, height} = e.getBoundingClientRect(); return {left, top, width, height}; }); log('[Element position]: ', JSON.stringify(pos, undefined, 2)); return pos; } 复制代码
OK,目前为止我们能可以对大部分的元素截图了,其余的是处于内滚动的元素
内滚动元素截图
内滚动:相对于传统的window窗体滚动,它的主滚动条是在页面(或者某个元素)的内部,而不是在浏览器窗体上。最常见的是在后台管理界面,左侧栏和右侧的内容区的滚动条是分开的。
想象一下,打开网易云音乐,首屏会出现两个内滚动条,如果我们想看到更多的歌单,需要将滚动条下滑。 内滚动截图也是同样的道理,结合页面滚动让目标元素暴露在可视范围内,再通过视窗坐标来达到精确截图。
步骤:
- 获取目标元素的坐标,判断其是否在当前可视范围内,如果在视窗内,则无需滚动
- 由于是内滚动,目标元素外面必定套了一层有滚动条的父元素,通过滚动该父元素来间接展示目标元素。所以这一步需要确定父元素的选择器
- 通过模拟页面滚动父元素(设置
window.scrollBy
或者scrollLeft scrollTop
),使目标对象刚好能完整地出现在视窗内 - 因为是内滚动,所以需要重新获取目标元素的坐标(
getBoundingClientRect
) - 利用新坐标截图
这儿有个小细节,关于如何判断元素是否有滚动条。如果元素无 X轴
滚动条,那么设置他的 scrollLeft
是没有效果的,这时只能全局滚动才行。
// 如果scrollWidth值大于clientWidth值,则可以说明其出现了横向滚动条 element.scrollHeight > element.clientHeight // 如果scrollHeight值大于clientHeight值,则可以说明其出现了竖向滚动条 element.scrollHeight > element.clientHeight 复制代码
示例代码:以Nodejs官方文档中的内滚动为例,获取左侧栏中TTY的截图
/** * 截取左侧栏中TTY所在的li节点 */ const path = require('path'); const puppeteer = require('puppeteer-core'); const log = console.log; (async () => { const browser = await puppeteer.launch({ executablePath: path.join(__dirname, './chromium/Chromium.app/Contents/MacOS/Chromium'), // 关闭headless模式, 会打开浏览器 headless: false, args: ['--no-sandbox', '--disable-dev-shm-usage'], }); const page = await browser.newPage(); await page.setViewport({width: 1920, height: 600}); const viewport = page.viewport(); // Nodejs官方Api文档站 await page.goto('https://nodejs.org/dist/latest-v10.x/docs/api/'); // await page.waitFor(1000); // 这里强烈建议使用 waitForNavigation,1000这中魔鬼数字会让代码变得不放心 await page.waitForNavigation({ // 20秒超时时间 timeout: 20000, // 不再有网络连接时判定页面跳转完成 waitUntil: [ 'domcontentloaded', 'networkidle0', ], }); // step1: 确定内滚动的父元素选择器 const containerEle = '#column2'; // step1: 确定目标元素选择器 const targetEle = '#column2 ul:nth-of-type(2) li:nth-of-type(40)'; // step1: 获取目标元素在当前视窗内的坐标 let pos = await getElementBounding(page, targetEle); // 使用内置的DOM选择器 const ret = await page.evaluate(async (viewport, pos, element) => { // step1: 判断目标元素是否在当前可视范围内 const sumX = pos.width + pos.left; const sumY = pos.height + pos.top; // X轴和Y轴各需要移动的距离 const x = sumX <= viewport.width ? 0 : sumX - viewport.width; const y = sumY <= viewport.height ? 0 : sumY - viewport.height; const el = document.querySelector(element); // strp3: 将元素滚动进视窗可视范围内 // 此处需要判断目标元素的x、y是否可滚动,如果元素不能滚动则滚动window // 如果scrollWidth值大于clientWidth值,则可以说明其出现了横向滚动条 if (el.scrollWidth > el.clientWidth) { el.scrollLeft += x; } else { window.scrollBy(x, 0); } // 如果scrollHeight值大于clientHeight值,则可以说明其出现了竖向滚动条 if (el.scrollHeight > el.clientHeight) { el.scrollTop += y; } else { window.scrollBy(0, y); } return [el.scrollHeight, el.clientHeight]; }, viewport, pos, containerEle); // step4: 由于目标元素在视窗外,且处于内滚动父元素内,所以需要重新获取坐标 pos = await getElementBounding(page, targetEle); // await page.waitFor(1000); // 这里强烈建议使用 waitForNavigation,1000这中魔鬼数字会让代码变得不放心 await page.waitForNavigation({ // 20秒超时时间 timeout: 20000, // 不再有网络连接时判定页面跳转完成 waitUntil: [ 'domcontentloaded', 'networkidle0', ], }); // 5. 截图 await page.screenshot({ path: 'scroll_and_bounding.png', type: 'png', clip: { x: pos.left, y: pos.top, width: pos.width, height: pos.height } }); await browser.close(); })(); 复制代码
三、踩过的坑:在 Linux
上安装 Chromium
事实证明:在 Linux 环境中安装Chromium的经历会无比难忘。 安装 puppeteer
时,会自动下载Chromium,由于众所周知的原因,下载常常以失败告终。换个镜像源后Chromium能下载成功,但启动后 各种报错,是Linux上缺少部分依赖导致的。安装完需要的依赖,代码顺利运行。但截图却发现浏览器上的中文字体竟全是框框框框。OK,安装字体库,中文字正常显示了!
踩坑后的最佳实践
- 采用
Chromium
和npm包
分开的方式,只安装puppeteer-core
,通过executablePath
引入自行下载的Chromium
,极大加快npm install
的速度。 - 将Linux的镜像源切换成阿里的镜像源,可以快速下载
Chromium
- 将项目改用
Docker
部署,避免出现本地开发正常,上线后却出现各种问题的情况 - 尽量避免使用
page.waifFor(1000)
,1000毫秒数只是毛估估的时间,让程序自己决定效果会更好
相关解决办法:
yum install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 ipa-gothic-fonts xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 xorg-x11-fonts-misc -y 复制代码
# 设置阿里镜像源 echo "https://mirrors.aliyun.com/alpine/edge/main" > /etc/apk/repositories echo "https://mirrors.aliyun.com/alpine/edge/community" >> /etc/apk/repositories echo "https://mirrors.aliyun.com/alpine/edge/testing" >> /etc/apk/repositories # 安装Chromium及依赖,包括中文字体支持 apk -U --no-cache update apk -U --no-cache --allow-untrusted add zlib-dev xorg-server dbus ttf-freefont chromium wqy-zenhei@edge -f 复制代码
安装完后需要去沙箱才能运行,尽管官方并不推荐。
Linux沙箱:在计算机安全领域,沙箱(Sandbox)是一种程序的隔离运行机制,其目的是限制不可信进程的权限。沙箱技术经常被用于执行未经测试的或不可信的客户程序。为了避免不可信程序可能破坏其它程序的运行。
-
--no-sandbox
: 去沙箱运行 -
--disable-dev-shm-usage
: 默认情况下,Docker
运行一个/dev/shm
共享内存空间为64MB 的容器。这通常对Chrome来说太小,并且会导致Chrome在渲染大页面时崩溃。要修复,必须运行容器docker run --shm-size=1gb
以增加/dev/shm
的容量。从Chrome 65开始,使用--disable-dev-shm-usage
标志启动浏览器即可,这将会写入共享内存文件/tmp
而不是/dev/shm
.
const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-dev-shm-usage'] }); 复制代码
四、通过 Docker容器
部署项目
项目干到最后,发现每次都需要安装Chromium,可能每次都会出现不可预料的问题出现。为了节约时间成本干更多有意义的事情,通过 shell脚本
和 Docker容器化
优化上述的部署流程。
Docker开发流程
Dockerfile Dockerfile Docker仓库 Docker容器
这里以部署一个基于 Puppeteer
的服务为例
确定基础镜像
# 在Docker Hub或私有仓库上搜索需要的镜像 docker search node 复制代码
前往Docker Hub能看到更详细的描述和版本
# 在这选择 `node:10-alpine` 为基础镜像 docker pull node:10-alpine 复制代码
编写 Dockerfile
(攻略不全,建议网上找更详细的资料)
FROM
: 指定基础镜像,必须是 Dockerfile
中的第一个非注释指令
FROM <image name> FROM node:10-alpine 复制代码
MAINTAINER
: 设置该镜像的作者
MAINTAINER <author name> (不推荐使用,推荐使用LABEL来指定镜像作者) LABEL MAINTAINER="zhangqiling" (推荐) 复制代码
RUN
: 在 shell 或者exec的环境下执行的命令。RUN指令会在新创建的镜像上添加新的层面,接下来提交的结果用在Dockerfile的下一条指令中
RUN <command> # RUN可以执行任何命令,然后在当前镜像上创建一个新层并提交 RUN echo "https://mirrors.aliyun.com/alpine/edge/main" > /etc/apk/repositories # 执行多条命令时,可以通过 \ 换行 RUN apk -U add \ zlib-dev \ xorg-server 复制代码
RUN
指令创建的中间镜像会被缓存,并会在下次构建中使用。如果不想使用这些缓存镜像,可以在构建时指定 --no-cache
参数,如: docker build --no-cache
。
CMD
: 提供了容器默认的执行命令。 Dockerfile
只允许使用一次 CMD
指令,如果存在多个 CMD
,也只有最后一个会生效
# 有三种形式 CMD ["executable","param1","param2"] CMD ["param1","param2"] CMD command param1 param2 复制代码
COPY
: 于复制构建环境中的文件或目录到镜像中
COPY <src>... <dest> COPY ["<src>",... "<dest>"] # 将项目复制到my_app目录下 COPY . /workspase/my_app 复制代码
ADD
: 也是复制构建环境中的文件或目录到镜像
ADD <src>... <dest> ADD ["<src>",... "<dest>"] 复制代码
相比 COPY
, ADD
的 <src>
可以是一个 URL
。同时如果是压缩文件, Docker
会自动解压。
WORKDIR
: 指定 RUN
、 CMD
与 ENTRYPOINT
命令的工作目录
WORKDIR /workspase/my_app 复制代码
ENV
: 设置环境变量
# 两种方式 ENV <key> <value> ENV <key>=<value> 复制代码
VOLUME
: 授权访问从容器内到主机上的目录
VOLUME ["/data"] 复制代码
EXPOSE
: 指定容器在运行时监听的端口
EXPOSE <port>; 复制代码
参考文档,感谢分享
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。