使用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
客户端对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 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 => { 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
对象的响应拦截器中。至于,如何解密,就要自己摸索了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| 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 => { 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
对象,用于方便存放全局对象,全局方法,任务个性化参数等等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
|
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
对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| 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
还是各种参数,所以应针对不同任务单独维护:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import service from "../utils/httpclient/request.js"; import USER_AGENT from '../utils/USER_AGENTS.js'
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站的今日用户经验
![](https://huanghedidi-img-repository.oss-cn-shenzhen.aliyuncs.com/20220702232724.png)
- 打开浏览器开发者工具-
network
,清空,选中Fetch/XHR
![](https://huanghedidi-img-repository.oss-cn-shenzhen.aliyuncs.com/20220702232815.png)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
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脚本
按照任务前
,任务执行
,任务后
和任务异常
的模式来编写
1 2 3 4 5 6 7 8 9
| let $ = new BILIBILIEnv('bilibili日常任务'); (async () => { await before(); await execute(); await after(); })().catch(reason => { $.addErrMsg(reason.stack); $.send(); });
|
支持配置文件控制任务参数
配置文件统一管理一些任务的参数,如各种api
必备的cookie
、token
等信息,也支持任务的启用和关闭
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| 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
实现呢?下面通过代码来说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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); }
|
1 2 3 4 5 6 7 8 9
|
async function execute() { let resp = await getDailyRewardInfo($.cookie); if (resp.data.code) { throw new Error('cookie已失效!'); } }
|
1 2 3 4 5 6 7
| (async () => { await execute(); })().catch(reason => { $.addErrMsg(reason.stack); $.send(); });
|
最后
上述自动化脚本已上传至github
仓库:automate-scripts
实现的功能有:
哔哩哔哩
- 一键完成日常任务,包括登录,签到,分享,可获得20经验
- 点赞关注的up主最新视频:支持参数配置是否开启此功能
- 投币关注的up主最新视频:支持参数配置是否开启此功能,以及一天最多的投币数量
- 数据汇总:汇总今日经验值等数据
京东
通知
- 目前仅实现企业微信的通知渠道,本人最常用且仅使用此通知渠道,后续考虑添加更多渠道
欢迎大家提出疑问,如果文中存在的不对的地方,望大家评论告知,希望大家的技术能越来越好,共同进步!!!