抓取选课网的绩点

微信小程序限制真多

Posted by tsengkasing on 2017-04-30

之前已经完成了验证码的识别
就想进一步把绩点也抓取下来吧
毕竟我的手机没有一个浏览器可以保存xuanke网的登录状态

绩点页面抓取

总览

我的目标是要抓取绩点页面并解析成一串JSON字符串返回,我把每一步都独立成一个函数并以promise的形式完成。

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
function GPA(token, cb = function(){}) {
const {token1, token2} = token;//token1和token2分别是用户名和密码字符串

//先行访问xuanke登录页,获取cookie
requestLoginPage().then(()=>{
return requestVerificationCode();//带着cookie获取验证码图片
}).then((obj)=>{
return parseVerificationCode(obj);//解析验证码并返回解析结果字符串
}).then((code) => {
return requestToLogin({token1, token2, code});//post用户名密码和验证码
}).then(() => { //如果用户名和密码正确会进行跳转,否则抛出异常
return requestLoginCheckCode();//跳转,判定验证码是否正确
}).then(() => { //如果验证码错误,抛出异常
return requestToHome(); //如果验证码正确,则会进入到xuanke网首页
}).then(() => {
return requestBeforeGetGPA(); //在获取绩点页面之前需要post一个请求(并不知道用意)
}).then(() => {
return requestGetGPA(); //获取服务端渲染好的绩点页面
}).then((html) => {
cb(parseGPA(html)); //解析绩点html,并返回JSON字符串,解析出错则返回错误信息
}).catch((message) => {
console.error(message); //处理异常,包括用户名密码错误和验证码错误
cb(message); //返回错误信息
});
}

访问xuanke登录页,获取cookie

这一步没什么,主要是先获取cookie并保存下来

1
2
3
let store = {};
//...省略
store.cookie = response.headers['set-cookie'].join('').split(';')[0];

带着cookie获取验证码图片并解析

目标路径是/CheckImage。

1
2
3
4
5
6
7
8
9
10
const opts = {
hostname: "xuanke.tongji.edu.cn",
path: "/CheckImage",
method: 'GET',
port: 80,
headers: {
//...省略一部分headers
'Cookie': store.cookie
}
};

获取到图片是buffer的形式,再调用我上一篇博文写好的parseCAPTCHA.js解析即可。

这里之前限制了必须是读取文件路径,所以我又对此稍稍改动了一下parseCAPTCHA.js文件让其支持读取buffer,因为使用的Jimp包本身提供了读取buffer的方法。

Post用户名密码和验证码

这里的目标是:“tjis2.tongji.edu.cn/amserver/UI/Login

会有5个Post参数要求,分别是

  1. goto
  2. gotoOnFail
  3. Login.Token1
  4. Login.Token2
  5. T3

goto是用户名和密码正确会接着跳转的页面

同理,gotoOnFail是错误要跳转的页面

Token1和Token2是用户名和密码

T3是四位验证码的字符串

此处的response会带有4个set-cookie的字段,然而其中一个是立即失效,剩余三个需要存储起来,之后需要使用。

分别是以下三个字段:

  1. AMAuthCookie
  2. amlbcookie
  3. IPlanetDirectoryPro

可以根据response的headers的Location字段判定是含有’pass.jsp’还是’deny.jsp’来明确用户名和密码是否正确,然后做异常处理。

跳转到验证码判定

如果上一步的用户名和密码正确,会跳转到’xuanke.tongji.edu.cn/pass.jsp?checkCode=${验证码}`

同以上一步,根据response的headers的Location字段是否含有’index.jsp’来判断验证码是否识别正确,错误要做异常处理。如果含有’index.jsp’证明验证码错误,跳转到登录页。否则成功。

进入登录后的首页

带着coookie
目标是’xuanke.tongji.edu.cn/tj_login/frame.jsp

发送一个不知用意何在的GET请求

带着cookie
请求路径是:
xuanke.tongji.edu.cn/tj_login/redirect.jsp?link=/tj_xuankexjgl/score/query/student/cjcx.jsp?qxid=20051013779916$mkid=20051013779901&qxid=20051013779916&HELP_URL=null&MYXSJL=null

获取绩点页面

带着cookie
GET请求路径是:
xuanke.tongji.edu.cn/tj_xuankexjgl/score/query/student/cjcx.jsp?qxid=20051013779916&mkid=20051013779901

这里response的data还是用Buffer处理,我使用了iconv-lite来将GBK的编码转成UTF-8,然后使用html-minifier将返回的html压缩预处理,去除空行和空格,方便之后的处理。

解析绩点html页面

使用了jsdom包来提取页面信息

这里的页面没有明显的id或者class可以直接提取,大部分信息我都要用到parentNode或者nextSilbing来获取我要的节点。

具体的提取方法,大家可以自己去看看页面结构,也许有比我更快的提取方法。

Electron

东西都爬完了当然想做成能使用的产品,想到了js写客户端的两个轮子分别是Electron和NW.js,觉得Atom使用的Electron可能更有优势于是去试了一下,把页面写好了之后觉得用客户端还不如直接打开edge上去看呢。。于是还是打算做成普通的网页吧。

Web页面

首先想到了browserify,天真的想着这样可以把所有东西都在浏览器跑了。
然而,咋就忘记了跨域的问题呢。顿时觉得自己还是智障。

于是还是按普通的做法,开个服务器,写个post的API。
戳这里

不想自己服务器再开一个后台

于是只是把这个API加到novastar的API中去了

微信小程序

在写完Web页面+后台API之后,在同心云上看到有人做了一个查电费的微信小程序(虽然我一直查不了

但还是觉得我也可以写一个玩玩,特别是现在小程序开放给个人用户了。

看到wxss就是css,wxml就是html的时候,觉得蜜汁神奇。

小程序整体的开发是按照React的思路,单向数据流。数据绑定之后可以通过一个setData()的函数来更新页面。笔者觉得如果熟悉React的话应该直接就上手了。

唯一要提的一点是发现网络请求一定要https协议,当时心想,天哪,SSL好贵啊大概是做不成了,然后去网上一搜,发现阿里云居然可以免费申请SSL证书,而且一个阿里云账户可以申请20个免费证书!非常震惊。

因为有人觉得我在偷密码XD,在这里贴出整个请求的代码,当然其实这还不够,所以觉得我偷密码的就不要用了orz。。
做这个小程序的其中一个原因是我的手机浏览器包括chrome、微信内置的等等都不能保存xuanke网的登录状态,期末的时候没电脑很难查。

以下是微信端发请求调用的函数,传入token,用户输入的学号密码,cb是回调函数。

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
function getGPA(token, cb) {
const {token1, token2} = token;

wx.setStorage({
key: "token",
data: token
})

wx.request({
url: 'https://novastar.everstar.xyz/api/gpa',
method: 'POST',
data: {
token1: token1,
token2: token2
},
dataType: 'json',
header: {
'content-type': 'application/json;charset=UTF-8'
},
success: function (res) {
if (typeof res.data === 'object') {
cb(res.data);
} else {
cb(res.data);
}
}
})
}

当时novastar是采用了node的express框架,这里直接添加了一个API,调用的GPA函数就是本文最上面代码的函数。

1
2
3
4
5
6
7
8
9
router.post('/gpa', function (req, res) {
const token1 = req.body.token1;
const token2 = req.body.token2;

GPA({token1, token2}, function (gpa) {
if(!gpa) res.send(JSON.stringify(false));
else res.send(gpa);
});
});

这里也放出一个可运行的Node版本戳这里

1
2
$ npm install
$ npm start

访问http://localhost:5321即可

不得不说现在小程序审核速度挺快的,我审核了两次都是24小时内就出结果了。(虽然五一的时候停了

同济大学绩点查询微信小程序已经审核通过上线了,欢迎各位校友使用:)

在小程序搜索’同济大学绩点查询’即可找到,也可扫描下方二维码↓