序
回忆这一年,匆匆如白驹过隙。不经想起杨绛先生曾说:“我们曾如此渴望命运的波澜,到最后才发现:人生最曼妙的风景,竟是内心的淡定与从容。”
述
Hi,大家下午好,今天是周五咯,也是21年的最后一天啦。趁着年末的尾巴。给大家来一个小知识分享~
是关于什么的呢?光看标题就知道啦,这么明显。哈哈哈哈,可恶,关子没卖出来。
好啦,那就直接进入正题吧~
文
在说之前,首先了解下大家对 puppeteer 这玩意了解多少。我最近用了用,是真的香。然后这篇文章的主旋律也是使用这个来做的。
哦,对了,主要的语言是nodejs。接下来就正式开始啦~
首先我们先新建一个项目:
// 新建文件夹
mkdir onlinerun
// 进入文件夹
cd onlinerun
// 初始化
npm init -y
// touch test.js
然后我们安装一些要用到的依赖:
npm install puppeteer node-schedule request -D
// 这里要补充一个小点嗷,就是如果npm很慢,或者失败,可以试一下指定一下淘宝镜像(如果你不想改npm源的话
// 具体如下:
npm install puppeteer --registry=https://registry.npm.taobao.org
我个人比较建议这种方式嗷,因为在开发的过程中,一般都是公司项目和个人项目混着开发,那么设置这个源到公司,或者淘宝,都太麻烦。
嘿嘿,不过也看个人喜好啦~
然后依赖装完了,就可以进入项目开发了,除了puppeteer 之外的两个依赖是干嘛用的,后面用到了,我们再讲。
先来看下puppeteer怎么玩,如果你之前有用过这个那就可以跳过这一段嗷。
// 一个爬虫的功能
const Puppeteer = require("puppeteer");
const log = console.log;
(async () => {
const browser = await Puppeteer.launch({
headless: true,
}).catch(() => browser.close);
const page = await browser.newPage();
await page.goto("https://www.baidu.com/").catch(() => browser.close);
let urls = await page.$$eval("a", (el) =>//图片节点,API可查看官方介绍
el.map((x) => x.href)
);
const obj = []
urls.forEach((item) => {
obj.push(item)
})
log(obj)
await browser.close();
})();
上面这个简单的例子,大家可以试一下,就是爬虫了。如果没问题,我们就开始说本期要说的监控了嗷。
既然是监控,那么监控什么呢。还需要使用到puppeteer?
不知道大家平时有没有在线上看到过,项目的资源文件挂掉了,尤其是图片资源这样的静态文件,然后就会像这样:
并且这种,你不可控,发生了,你不去管,没有用户上报,你压根就不知道,然后人有它在线上挂。哈哈哈哈哈哈
还有另一种情况就是,现在开发的前端框架,要么是vue,要么是react。那么就会有一些console上的依赖报错啦,告警啦。当然刚刚那种资源挂掉了,console也是会暴露的。举个例子:
那么遇到告警warning,其实不那么讲究,也可以不去管了,毕竟不影响主进程,程序一样的在用的。
那么要是讲究一点,就可以把这些给解决了。强迫症表示,console干干净净的,看着心里舒坦。哈哈哈哈哈哈
但是报错,error 有一说一,绝对不能忍。因为这个说明你的程序有问题了,可能页面正常渲染了,功能看下来也没问题,但是隐藏的bug,是肯定存在的。
那么就关于这两个问题,都是那种开头说的,不去管,肯定不知道,除非等用户上报。那么怎么去管呢?线上那么多页面呢。这不是开玩笑呢吗。哎嘿,这个时候,puppeteer ,就派上用场了。
还记得我们刚开始小试牛刀的爬虫吗?如果没好好看建议再去看一眼嗷,接下来会用上。
那么回到上个问题,怎么去监控线上那么多页面,首先需要用到爬虫,先给几个核心页面接入进去。改动就是把刚刚的代码封装一下,url写到conf中去。
然后在这些核心页面中爬出来的url如果是二级页面,继续遍历。如果只是跳转链接,那就直接return。
这些操作过后你会得到一个url的集合。这些url后面会有很大的用处。
说到这边url遍历存储啊,推荐三种方法去做
-
落库,你可以使用mysql,或者mongo等,将页面中的url全都放到数据库中,然后在设计一个接口返回出来;
-
写文件,就是将url写到本地项目的某个文件,在后面去做监控的时候,直接读文件,code如下:
const fs = require("fs"); fs.appendFile(imgUrl + 'urls.json', JSON.stringify(obj), 'utf8', function(err, ret) { if (err) { throw err } console.log('success') })
-
就是每次都爬一次最新的,爬完就去监控。
好了,这三种方法,我最推荐的是第一种方法,其实说到做法,可以分为两步,我的方案啊,大家看情况参考即可:暴露一个接口,这个接口干嘛用呢?
这个接口,就是给监控调用,来获取url去进行循坏便利监控。然后,在请求这个接口的时候,去获取的数据是数据库里已有的数据,但是同时,会触发爬虫的方法。
为什要出发爬虫的方法呢?因为你本地调用的数据是上次爬虫到的,那么这次你请求了,就要把数据库里的数据跟新一下。给下次用。
那么有同学会说,那你这个没有时效性啊。并且,数据的读写会影响吗?
首先啊,时效性其实不用担心,因为你部署上服务了,那么肯定就是一直在跑的。数据也不存在很久的空窗期。然后读写一致的问题,你每次读完接口,吐完数据,才会去更新你数据库的数据,这个时候,你都拿到已有的数据了,库里该更新更新。也不必担心啦。
好了,关于这块,我就说完啦,如果你有更好的方法,可以交流嗷。接下来我么说关键的。直接看代码:
/*
* IMT project
*/
const puppeteer = require("puppeteer");
var sendrequest = require("request");
const schedule = require("node-schedule");
let nowurls = [];
const log = console.log;
function delay(time) {
return new Promise(function (resolve) {
setTimeout(resolve, time);
});
}
// get page dom
const asynconlineasserts = async (newurl) => {
try {
const cookiesarr = [
{
name: "uid",
value: "xxxxxxxxxxx",
domain: ".xxxxxxx.com",
path: "/",
expires: Date.now() + 3600 * 1000,
},
...
];
puppeteer
.launch({
headless: true, // 开启界面,
timeout: 60 * 1000,
// devtools: true, // 开启开发者控制台
//设置每个步骤放慢200毫秒
slowMo: 200,
ignoreHTTPSErrors: true,
//设置打开页面在浏览器中的宽高
defaultViewport: null,
args: ["--start-maximized", "--no-sandbox"],
ignoreDefaultArgs: ["--enable-automation"],
// executablePath: "/usr/bin/google-chrome",
})
.then(async (browser) => {
try {
const page = await browser.newPage();
await page.setDefaultNavigationTimeout(0);
const headlessUserAgent = await page.evaluate(
() => navigator.userAgent
);
const chromeUserAgent = headlessUserAgent.replace(
"HeadlessChrome",
"Chrome"
);
await page.setUserAgent(chromeUserAgent);
await page.setExtraHTTPHeaders({
"accept-language": "zh-CN,zh;q=0.9",
});
const openoptions = {
timeout: 0,
waitUntil: [
"load", //等待 “load” 事件触发
"domcontentloaded", //等待 “domcontentloaded” 事件触发
"networkidle0", //在 500ms 内没有任何网络连接
"networkidle2", //在 500ms 内网络连接个数不超过 2 个
],
};
// 设置cookie
await page.setCookie(...cookiesarr);
await delay(5000);
await page.reload();
await delay(5000);
page.on("response", (response) => {
const status = response.status().toString();
if (status.startsWith("4")) {
// log(response.url());
// log(response.status());
var differror = {
method: "POST",
url: "https://xxxxxxxxxxxxxx",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
msgtype: "text",
text: {
content:
newurl +
` 该URL在请求过程中存在部分资源异常,详情如下: ` +
response.url() +
` 状态: ` +
response.status(),
mentioned_list: [""],
mentioned_mobile_list: [""],
},
}),
};
sendrequest(differror, function (error, response) {
if (error) throw new Error(error);
log(response.body);
});
}
});
page.on("console", (msg) => {
// log("msg._type: ", msg._type);
if (msg._type === "error" || msg._type === "warning") {
// log("errormsg: ", JSON.stringify(msg));
var differror = {
method: "POST",
url: "https://xxxxxxxxxxx",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
msgtype: "text",
text: {
content:
newurl +
` 该URL请求过程中console存在部分报错具体如下: ` +
JSON.stringify(msg),
mentioned_list: [""],
mentioned_mobile_list: [""],
},
}),
};
sendrequest(differror, function (error, response) {
if (error) throw new Error(error);
log(response.body);
});
}
});
await page.goto(newurl, openoptions).catch(() => browser.close);
await browser.close();
try {
} catch (e) {
log(e);
}
} catch (e) {
log(e);
}
await delay(3000);
await browser.close();
});
} catch (e) {
log(e + "//////////");
}
};
function geturls() {
var options = {
method: "GET",
url: "http://10.xxx.xxx.xxx:2727/getonlineurls",
headers: {},
};
sendrequest(options, function (error, response) {
if (error) throw new Error(error);
const obj = JSON.parse(response.body).data;
nowurls = obj;
});
}
let startnum = 0;
let rule = new schedule.RecurrenceRule();
let times = [];
for (let i = 1; i < 60; i++) {
times.push(i);
}
rule.minute = times;
// 启动任务
let job = schedule.scheduleJob(rule, async () => {
// foreach
try {
log("startnum: ", startnum);
let oldlen = nowurls.length;
log("oldlen: " + oldlen);
if (oldlen > 0 && startnum < oldlen) {
if (nowurls[startnum].includes("login")) {
startnum++;
} else {
await asynconlineasserts(nowurls[startnum]);
startnum++;
}
} else {
startnum = 0;
geturls();
}
} catch (e) {
log(e + "---------");
}
});
好啦,这里也要解释下,刚刚除了puppeteer意外的依赖是干嘛用的了,一个是发送请求的,request,这个主要是我使用的刚刚爬虫数据存储的第一种方法,所以暴露了一个接口出来。
另一个就是,就是用作内部的告警使用,来发送报警请求,企业微信。哈哈哈哈哈。
另一个依赖呢,就是任务,这个是干嘛的呢,其实就是对node服务的一个任务命令,你可以根据你的需求来制定一个定时任务,抽离出来看一下:
const schedule = require("node-schedule");
let rule = new schedule.RecurrenceRule();
const log = console.log;
let times = [];
for (let i = 1; i < 60; i++) {
times.push(i);
}
rule.minute = times;
// 启动任务
let job = schedule.scheduleJob(rule, async () => {
// foreach
try {
log('新年快乐')
} catch (e) {
log(e + "---------");
}
});
大概意思,就是每一分钟打印一次 ‘’新年快乐‘’。
好了,回到主旋律,刚刚贴出来的代码的核心其实就是两块:
page.on("response", (response) => {
const status = response.status().toString();
if (status.startsWith("4")) {
log(response.url());
log(response.status());
}
});
这个主要是监控每个请求下去的全量response的返回,可以根据自己的需求,监控response里面的内容,然后去做逻辑。另一个呢就是:
page.on("console", (msg) => {
// log("msg._type: ", msg._type);
if (msg._type === "error" || msg._type === "warning") {
log("errormsg: ", JSON.stringify(msg));
}
});
监控每个页面请求完成了,console下面的情况,监控报错啊,告警啊,这些。这些相关也是根据自己的业务场景需求去做相关的逻辑设计啦。
关于这块的文档,给大家贴出来 使用手册
然后关于最后的告警方式,其实根据自己的需求来做,你可以选择报告,可以选择邮件,可以选择告警机器人。
个人觉得告警或者邮件,比较合适,本身就是监控线上相关的,那么出问题第一时间告警,及时解决呀~
关于puppeteer,说一个小点,就是关于 executablePath 这个参数。这个主要是干嘛呢,就是去指定你的服务上的chrome路径的,如果不指定也没关系,他会用自带的chromnium
但是我不喜欢用自带的,哈哈哈哈。这一切搞好之后,就可以往你的服务上部署啦。
最后执行;
node test.js
关于node启动,大家有时间可以去看下 forever 这个工具。
最后的效果:
末
好啦,关于这一期的内容就说完啦,谢谢你能看到这里嗷~
今天是21年的最后一天啦,提前祝大家,跨年开心,新年开心。
嘿嘿~ 拜拜👋~