小伙伴们的语雀频道html
Puppeteer
介绍及安装
Puppeteer
是一个Node
库,它提供了一个高级API来经过DevTools协议控制Chromium
。 在谷歌推出这款headless
浏览器后,Selenium
直接被我抛弃了,由于Puppeteer
对于Nodejs
开发者来讲简直太友好了,(正常状况下)只须要npm i puppeteer
,便可完成安装,而不须要安装其余的依赖库(当初太年轻o(╥﹏╥)o,其实并不简单)。node
系统环境的话在工做时使用MacOS,部署到服务器上的是Centos 7. 在MacOS
上确实简单,只须要npm i puppeteer
就行。安装不了有下列几条解决办法:linux
# 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
浏览器时需增长以下配置项git
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'],
});
复制代码
在截图或者爬虫时,经常遇到一些页面采用懒加载的方式展现数据,首屏是不会展现所有的信息给咱们。 针对懒加载,采用滚动到底的方式来破解。 啥?懒加载没有底,尝试直接调他们的接口吧,或者还有其余高明的方式欢迎指出web
page.evaluate(pageFunction, ...args): 该函数能让咱们使用内置的DOM选择器docker
这里要特别注意下pageFunction
的传参方式为:shell
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"
复制代码
代码:以简书的懒加载为例npm
/** * 懒加载页面自动滚动 */
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
)定位截图。固然了,元素选择器必需要找准,不然再怎么样也没法精确截图json
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
的速度。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>;
复制代码
Dockerfile
样例https://mirrors.aliyun.com/alpine/edge/testing/
能找到npm install
会报错,设置 npm config set unsafe-perm true
后能顺利安装,这是什么缘由?(MacOS上的docker没这个问题)# 拉取node镜像
FROM node:10-alpine
# 设置镜像做者
LABEL MAINTAINER="qiyang.hqy@dtwave-inc.com"
# 设置国内阿里云镜像站、安装chromium 6八、文泉驿免费中文字体等依赖库
RUN echo "https://mirrors.aliyun.com/alpine/v3.8/main/" > /etc/apk/repositories \ && echo "https://mirrors.aliyun.com/alpine/v3.8/community/" >> /etc/apk/repositories \ && echo "https://mirrors.aliyun.com/alpine/edge/testing/" >> /etc/apk/repositories \ && apk -U --no-cache update && apk -U --no-cache --allow-untrusted add \ zlib-dev \ xorg-server \ dbus \ ttf-freefont \ chromium \ wqy-zenhei@edge \ bash \ bash-doc \ bash-completion -f
# 设置时区
RUN rm -rf /etc/localtime && ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
# 设置环境变量
ENV NODE_ENV production
# 建立项目代码的目录
RUN mkdir -p /workspace
# 指定RUN、CMD与ENTRYPOINT命令的工做目录
WORKDIR /workspace
# 复制宿主机当前路径下全部文件到docker的工做目录
COPY . /workspace
# 清除npm缓存文件
RUN npm cache clean --force && npm cache verify # 若是设置为true,则当运行package scripts时禁止UID/GID互相切换
# RUN npm config set unsafe-perm true
# 安装pm2
RUN npm i pm2 -g
# 安装依赖
RUN npm install
# 暴露端口
EXPOSE 3000
# 运行命令
ENTRYPOINT pm2-runtime start docker_pm2.json 复制代码