使用nodejs编写自动化脚本,真香!
说到写脚本,最为人熟知的语言必然是shell
,再者python
,当然现在也出现了很多界面友好,支持可视化拖动编写脚本的软件,如quiker
等。但本文要介绍的是nodejs
,其用到的语言是JavaScript
,本人最近正在学习。nodejs
支持通过命令的方式执行JavaScript
脚本文件,脱离了浏览器环境,使得编写脚本成为可能。JavaScript
语言生态相当丰富,社区活跃,当需要实现某个创意时丰富的生态可助力快速落地。
自动化脚本
一般写脚本把繁琐重复的事情一键完成,配合一定的运行机制,如定时任务调起脚本,使其自动运行,大大减轻工作负担。在工作中可能会写自动化部署项目的脚本,定时监控系统运行的脚本,定时清理文件的脚本等,但是如果个人呢?很多人每天都忙碌于各种app
的签到,完成app
的日常任务,查看视讯动态等,这些工作要是也能自动化运行且主动通知就好了,仿佛996
的生活也出现了一丝惬意。
搭建自动化任务脚手架
要实现各种app
的签到、完成日常任务及消息通知等功能最核心的是编写http
客户端,http
是目前最最广泛的应用协议,http
客户端在nodejs
的世界里实在太多了,本人选择的是axios
,其被广泛使用。
本人参考了一些开源的自动化任务项目,且实践编写,总结了几个自动化任务脚手架的要点:
-
http
客户端,用于发送和接收请求 - 分任务维护
Env
对象,存放任务个性化参数 - 分任务设计
api
文件,便于维护api
信息 - 任务脚本编写模式需高度统一,便于新增和维护
- 配置文件形式维护任务参数
http客户端
这里用到的是axios
框架,是很火的nodejs
生态的http
客户端工具,一般根据业务封装一个http
客户端对象:
// 封装axios
import axios from "axios";
axios.defaults.headers["Content-Type"] = 'application/json;charset=utf-8'
const service = axios.create({
baseURL: '',
timeout: 30000
})
service.interceptors.request.use(config => {
// 如果是get请求将config的params域拼接至url中
if (config.method === 'get') {
let url = config.url + '?';
if (config.params && typeof config.params !== "undefined") {
for (let key of Object.keys(config.params)) {
let part = encodeURIComponent(key) + '=';
if (config.params[key]) {
part += encodeURIComponent(config.params[key]) + '&';
url += part;
}
}
}
config.url = url.slice(0, -1);
config.params = {};
}
return config;
}, error => {
console.log(error)
Promise.reject(error)
})
export default service
针对不同任务的app
应维护不同的http
客户端对象,因为有些app
的响应报文是加密的,其解密过程可直接封装在axios
对象的响应拦截器中。至于,如何解密,就要自己摸索了。
// 封装对需要对响应报文解密的http客户端
import axios from "axios";
import CryptoJS from "crypto-js";
function aesDecrypt(data, aesKey = 'xxxxxxxxxxx') { //解密
if (data.length < 1) {
return '';
}
let key = CryptoJS.enc.Utf8.parse(aesKey);
let decrypt = CryptoJS.AES.decrypt(data, key, {mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7});
let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
return decryptedStr;
}
axios.defaults.headers["Content-Type"] = 'application/json;charset=utf-8'
const service = axios.create({
baseURL: '',
timeout: 30000
})
service.interceptors.request.use(config => {
// 如果是get请求将config的params域拼接至url中
if (config.method === 'get') {
let url = config.url + '?';
if (config.params && typeof config.params !== "undefined") {
for (let key of Object.keys(config.params)) {
let part = encodeURIComponent(key) + '=';
if (config.params[key]) {
part += encodeURIComponent(config.params[key]) + '&';
url += part;
}
}
}
config.url = url.slice(0, -1);
config.params = {};
}
return config;
}, error => {
console.log(error)
Promise.reject(error)
})
service.interceptors.response.use(resp => {
if (resp.data.code === 1) {
var serializer_data = aesDecrypt(resp.data.data);
resp.data.data = JSON.parse(serializer_data);
}
return resp.data;
}, error => {
console.log(error)
Promise.reject(error)
})
export default service
Env对象
设计了Env
对象的概念,每个任务都有自己的Env
对象,用于方便存放全局对象,全局方法,任务个性化参数等等
// Env对象的基类,封装共有方法
import {SEPARATOR_LINE} from "./../constants.js";
import {notifyAll} from "./../notifyUtils.js";
import {now} from "./../common.js";
export default class BaseEnv {
constructor(name) {
this.name = name;
this.cookie = '';
this.detailMsg = [];
this.errMsg = [];
}
addDetailMsg(msg) {
this.detailMsg.push(msg);
}
addErrMsg(msg) {
this.errMsg.push(msg);
}
async init() {
console.log('需子类重写');
}
getUserInfo() {
return '需子类重写\n';
}
async send() {
let content = `【当前时间】:${now()}\n`;
content += this.getUserInfo();
content += `【明细】:\n`;
if (this.detailMsg.length > 0) {
content += `${this.detailMsg.join('\n')} \n`
} else {
content += `无明细内容\n`;
}
content += `${SEPARATOR_LINE}【异常】:\n`;
if ((this.errMsg.length > 0)) {
content += `${this.errMsg.join('\n')}`;
} else {
content += `无异常\n`;
}
await notifyAll(this.name, content);
}
}
针对B站
的任务封装Env
对象
import config from "../../config.js";
import {getAccountInfo} from "../../api/bilibili.js";
import BaseEnv from "./BaseEnv.js";
export default class BILIBILIEnv extends BaseEnv {
constructor(name) {
super(name);
this.name = name;
this.mid = '';
// 我的昵称
this.uname = '';
// 我的会员等级
this.rank = '';
this.likeUpVideo = false;
this.custCoin = false;
this.maxCustCoinNum = 0;
}
async init() {
this.cookie = config.bilibili.cookie;
this.likeUpVideo = config.bilibili.likeUpVideo;
this.custCoin = config.bilibili.custCoin;
this.maxCustCoinNum = config.bilibili.maxCustCoinNum;
let {data} = await getAccountInfo(this.cookie);
console.log('获取到的数据是:', data);
if (data.data && data.data.uname) {
this.uname = data.data.uname;
this.mid = data.data.mid;
this.rank = data.data.rank;
} else {
throw new Error('cookie已过期!!');
}
}
getUserInfo() {
return `【当前用户】:${this.uname}\n【当前等级】:${this.rank}\n`;
}
}
编写任务的api文件
不同任务的api
自然是不同的,无论是url
还是各种参数,所以应针对不同任务单独维护:
// bilibili相关的api写在此文件下
import service from "../utils/httpclient/request.js";
import USER_AGENT from '../utils/USER_AGENTS.js'
/**
* bilibili个人用户信息
*/
export function getAccountInfo(cookie) {
const options = {
url: "http://api.bilibili.com/x/member/web/account",
headers: {
Accept: "*/*",
Connection: "keep-alive",
Cookie: cookie,
"User-Agent": USER_AGENT,
"Accept-Language": "zh-cn",
}
};
return service(options);
}
各项api
参数如何获取就需要大家自己摸索了,有些api
是官方公开的,GitHub
上也有大佬分享些自己摸索出来的api
,当然自己也可以使用浏览器开发工具抓包获取需要的api
,下面举个例子:
获取B站的今日用户经验
- 登录个人中心-我的记录-经验记录
- 打开浏览器开发者工具-
network
,清空,选中Fetch/XHR
- 刷新页面后观察抓包界面获取请求信息
- 根据抓包的信息编写
api
文件,一般重点的是url
、请求方式
、请求头关键带上cookie
或者token
之类的信息
/**
* 查询经验值的变动记录
* @param cookie
* @returns {AxiosPromise}
*/
export function queryExpLog(cookie) {
const options = {
url: " https://api.bilibili.com/x/member/web/exp/log?jsonp=jsonp",
headers: {
Accept: "*/*",
Connection: "keep-alive",
Cookie: cookie,
"User-Agent": USER_AGENT,
"Accept-Language": "zh-cn",
}
};
return service(options);
}
编写task脚本
按照任务前
,任务执行
,任务后
和任务异常
的模式来编写
let $ = new BILIBILIEnv('bilibili日常任务');
(async () => {
await before();
await execute();
await after();
})().catch(reason => {
$.addErrMsg(reason.stack);
$.send();
});
支持配置文件控制任务参数
配置文件统一管理一些任务的参数,如各种api
必备的cookie
、token
等信息,也支持任务的启用和关闭
export default {
"jd": {
"cookie": "pt_pin=xxxxxx;pt_key=xxxxxxx;"
},
"bilibili": {
"cookie": "SESSDATA=xxxxxxx;DedeUserID=xxxxxxx;",
"likeUpVideo": true,
"custCoin": true,
"maxCustCoinNum": 2
},
"fastcat": {
"token": "xxxxxxx"
},
"notify": {
"qywx": {
"open": true,
"token": {
"corpid": "xxxxxxx",
"corpsecret": "xxxxxxx",
"touser": "xxxxxx",
"agentid": "xxxxxxx",
"mediaId": "xxxxxxx",
"sage": "0"
},
}
}
}
技术要点总结
使用nodejs
写脚本的过程让我更了解这门技术,感受到其既强大又便利,感受到与Java
这类编译型语言不同的编程体验。
上述自动化脚本无非是发送http
请求和接受http
请求,最最核心的技术是nodejs
的异步编程机制,大量使用到es6
中的Promise
类型。在脚本中最常见的一种场景是:
发送
http
请求,等待响应结果,如果正常执行下一个http
请求,如果异常直接抛异常结束任务。
怎么通过Promise
实现呢?下面通过代码来说明:
// 函数返回的是Promise对象
export function getDailyRewardInfo(cookie) {
const options = {
url: "http://api.bilibili.com/x/member/web/exp/reward",
headers: {
Accept: "*/*",
Connection: "keep-alive",
Cookie: cookie,
"User-Agent": USER_AGENT,
"Accept-Language": "zh-cn",
}
};
return service(options);
}
// 异步函数体内使用await参数,await参数后必须接Promise类型
// 其等待Promise落定后才向后执行
async function execute() {
let resp = await getDailyRewardInfo($.cookie);
if (resp.data.code) {
throw new Error('cookie已失效!');
}
}
// 即时执行的异步函数调起异步函数,用到了await参数,因异步函数的返回结果必是Promise类型
(async () => {
await execute();
})().catch(reason => {
$.addErrMsg(reason.stack);
$.send();
});
最后
上述自动化脚本已上传至github
仓库:automate-scripts
实现的功能有:
- 哔哩哔哩
- 一键完成日常任务,包括登录,签到,分享,可获得20经验
- 点赞关注的up主最新视频:支持参数配置是否开启此功能
- 投币关注的up主最新视频:支持参数配置是否开启此功能,以及一天最多的投币数量
- 数据汇总:汇总今日经验值等数据
- 京东
- 京东商城的京豆签到
- 通知
- 目前仅实现企业微信的通知渠道,本人最常用且仅使用此通知渠道,后续考虑添加更多渠道
欢迎大家提出疑问,如果文中存在的不对的地方,望大家评论告知,希望大家的技术能越来越好,共同进步!!!