前言
暑假留校,天气炎热,宿舍只有一个小风扇,无奈只能去图书馆蹭空调,结果抢不到座位。于是开始研究并开发此小demo。预期目标是搞出一个完整的前后端并且能为多人使用,前端打算用Vue实现,后端用Node.js。
因为之前没学过,只能边学边写,尽力而为。
分析流程
1.抓包抢座请求:
http://wechat.v2.traceint.com/index.php/reserve/get/libid=10287&mlnlmn=12,8&yzm="
是一个Get请求,携带3个参数。
- libid====对应馆号
- mlnlmn====每次请求都会改变的一个hex_code
- 12,8====每个座位号对应的编码
- yzm====验证码,多次异常请求时携带,平时为空。先不用管。
2.得到场馆号和座位编码:
选择不同场馆多次抓包得到场馆对应值:
cgid=[10287,10288,10290,10291,10292,11324 ]
namelist=['弘毅自习室','外文期刊阅览室','工科图书阅览室','文理图书阅览室','科技期刊阅览室','明德自修室']
座位编码:
<div class="grid_cell grid_7" data-key="6,7" style="left:280px;top:245px;">
<em>窗</em>
<div class="grid_cell grid_1" data-key="7,13" style="left:490px;top:280px;">
<em>13</em>==可以看到13号座位对应7,13
从返回的html中得知 grid_1 这一类别对应的是座位号和编码,可以用正则表达式提取。
3.hex_code
每次请求都会改变,发现是调用了选择座位那个页面底部的一个js,到时候同样可以模拟调用来得到。
分析完毕
实现思路
- 前台为web界面,用户需要填写场馆号、座位号、Cookie、抢座时间,然后发送任务将表单传到后端。
- 前端界面通过Axios通讯,可以实时显示后台信息。
- 后端node.js 监听一个端口,将前端传来的信息处理保存,并用cron定时任务调用主函数。
- 主函数实现功能:向"我去图书馆"发送抢座的http请求
- 额外功能:每日签到,退座保护,cookie保活,监听空座。
后端实现
后端参考(抄)Github上的GoLib.js项目。
1.创建web服务器
监听8081端口,接受并处理post过来的表单,后台写死了一个验证码,验证通过后会调用main()函数,返回内容。
/*
http监听服务器,检测到用户提交后,就调用主函数
*/
const http = require('http');
var querystring = require('querystring');
const hostname = '127.0.0.1';
const port = 8081;
let sess_id;
let timearr;
let room_id;
let seat_no;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html;charset=utf-8');
if (req.url !== "/favicon.ico") {
var str = '';
req.on('data', (chunk) => {
str += chunk
})
req.on('end', () => {
var obj = querystring.parse(str);
if (obj.passid == 'handsomeboy') {//你能发现吗?这是写死的验证码
sess_id = obj.sessid;
timearr = obj.time.split(":");
room_id = obj.cgid;
seat_no = obj.seatid;
main()//执行主函数
res.end('<h1 align="center">已经提交到后台,请静候佳音!</h1>')
} else {
res.end('<h1 align="center">输入表单有误,请重新提交</h1>')
}
})
}
})
server.listen(port, hostname,)
2.主函数
获取当前的日期,在加上前端传过来的抢座时间组成定时任务,定时执行抢座函数cronReserve(),需要传入场馆号、座位编码、是否退座保护。
/*
主函数
*/
async function main() {
var sessid = sess_id;
var roomid = room_id;
var seatno = seat_no;
var date = new Date();
var nowDate = date.getDate();
var nowMonth = date.getMonth();
console.log(`将在 ${nowMonth + 1}月${nowDate}日${timearr[0]}:${timearr[1]}:${timearr[2]}预定 ${roomid} 号房间的 ${seatno} 号座位`);
let user = new LibUser(sessid);
await user.init();
new cron.CronJob(//设置定时器
`${timearr[2]} ${timearr[1]} ${timearr[0]} ${nowDate} ${nowMonth} * `,
async function () {
user.cronReserve(roomid, seatno, autoCancel = true);
},
null,
true,
process.env.TZ,
)
}
3.抢座函数
设置抢座次数attemps为3,遍历参数中的座位列表,如果当前是未成功的状态并且抢座次数大于0,就发送抢座请求并返回服务器响应状态。
cronReserve(roomid, seatno, autoCancel = false) {
let libuser = this;
async function asyncCall() {
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
let attemps = 3;//默认抢 3次,成功或者超出次数停止
let success = false;
for (var i = 0; i < seatno.length; i++) {
if (!success) {
console.log("准备抢:", roomid[i], "号阅览室", seatno[i], "号座")
}
while (!success && attemps > 0) {
attemps--;
let ret = await libuser.user.reserve(roomid[i], seatno[i]).catch(err => console.error(err.stack));
let reason = ret ? ret['msg'] : false;
success = ret ? ret['success'] : false;
if (success) {
console.log("座位预定成功!");
} else {
console.log("座位预定失败,原因是: " + reason);
}
await sleep(200);
}
attemps = 3
}
if (autoCancel) {
libuser.autoCancel();
}
}
asyncCall()
}
};
/*
发送抢座请求:
*/
async reserve(roomid, seatno) {
let seats = await this.getSeats(roomid);
if (!seats)
return {
success: false,
msg: "无此场馆",
response: null
};
let seatid = seats[seatno]
if (!seatid)
return {
success: false,
msg: "无此座位",
response: null
};
// 此处为服务器脚本提供一个隔离的沙箱环境
// 为保证在异步操作中,返回值能够被成功接收,此处预先定义一个空的resolve函数
let resolve = () => {
};
const vm_context = {
AJAX_URL: "https://wechat.v2.traceint.com/index.php/reserve/get/",
T: {
ajax_get: async (url, callback) => {
let response = await this.httpGet(url)
resolve({
success: response['code'] == 0,
msg: response['msg'],
response: response
});
}
}
};
// 在指定房间的网页中,寻找reserve_seat所在的js文件
let html = await this.httpGet("http://wechat.v2.traceint.com/index.php/reserve/layout/libid=" + roomid + ".html");
let regexp = /https?:\/\/[!-~]+\/[0-9a-zA-Z]+\.js/g;
let m = false, reserve_seat = null;
while (m = regexp.exec(html)) {
let jsUrl = m.toString()
let jsContent = await this.httpGet(jsUrl);
// js文件特征:内容含有“reserve_seat”、“T.ajax_get”
if (jsContent.search("reserve_seat") != -1 && jsContent.search("T.ajax_get") != -1) {
// 在虚拟机中执行js文件,并返回reserve_seat函数
reserve_seat = this._reserve_seat_func(jsContent, vm_context);
}
}
if (typeof reserve_seat != 'function')
return {
success: false,
msg: "找不到预定函数,可能场馆暂未开放。",
response: null
};
// 将真正的resolve函数赋值至作用域,执行reserve_seat
return await new Promise(function (r, reject) {
resolve = r;
reserve_seat(roomid, seatid);
});
}
4.正则表达式爬座位列表
class="grid_cell grid_1"的标签对应的是座位号,这一部分比较简单用python写的。
def getseatlist(cgid):#获取座位对应的id,传入的cgid是场馆号
url = 'http://wechat.v2.traceint.com/index.php/reserve/layoutApi/action=settings_seat_cls&libid=%s.html'%(cgid)
response = requests.get(url=url,headers=mheaders,verify=False)
response.encoding = 'utf8'
id = re.compile(#正则表达
r'grid_1" data-key="(.*?)" style=".*">\n<em>(.*)</em>')
id = id.findall(response.text)#得到一个一一对应的list
for i in list(id):
seat[i[1]]=i[0]
# print(seat,'\n')#最后生成字典并返回
return seat
顺便吧每日签到积分也搞了
def usertask():#每日签到积分任务
url1 = 'http://wechat.v2.traceint.com/index.php/usertask/index.html'#获取任务界面的地址
url2='http://wechat.v2.traceint.com/index.php/usertask/ajaxdone.html'#每日签到积分地址
response = requests.get(url=url1, headers=mheaders, verify=False).text
id = re.compile(#这一步是在正则表达式获取每日刷新的签到 ID
r'<td class="td-d"><button .* id="(.*?)">')
id = id.findall(response)[0]
data = {'id': id}
response = requests.post(url=url2, data=data, headers=mheaders, verify=False)
print(response.text.encode('utf-8').decode('unicode_escape'))
前端实现
。。。
算是不丑吧,把宝塔css偷过来了,勉强是能用了。
部署(宝塔面板):
1.将整个项目目录拷贝到服务器,然后打开终端执行:
npm install #安装依赖
2.安装 PM2管理器,使node项目可以后台运行,出错后可以自动重启。
添加一个项目
启动文件就选上传目录中的js文件,其余会自动识别,内存上限填100MB比较合适。
3.部署完毕:
打开web界面发送抢座任务,查看运行日志。
抢到座了
默认是开启退座保护的,我们不去签到,到时自动触发。
Talk is cheap,学习之路漫漫。少年继续加油吧!!!!!!