爬虫模拟登陆教务系统抓取成绩


本案例适用于基于 新版“湖南强智科技” 的教务系统(全国200多所高校),实践于“南昌大学网络教务系统” http://218.64.56.18/xk/

新版教务系统终于上线了,终于修复了古老浏览器才能登录使用的老毛病,由于自己正在做一款Android校园客户端,需要实现“一键查成绩”,遂开始了对教务系统的抓取。

教务系统属于登录类型,需要查看后台post数据。

分析

工具:Fiddler

1. 设定请求过滤

设定为教务系统的host:218.64.56.18,这个地址不同的学校不同

2. 登录系统

可以看到请求中post方式提交了username和password,使用了session来保存会话。 并没有马上返回网页,而是在response中使用了302重定向,同时将session传递过去,从此,该用户的会话建立,用这个session走通全站!看来新版的教务系统比老版的还简单!

3. 查看成绩

直接进入查看成绩模块,选择学期: 2015-2016-1,点击查询,成绩就在网页上显示了,现在来看数据。

这里直接定位到了 http://218.64.56.18/kscj/cjcx_list,这个地址就是最终成绩呈现的地址。

# request:
  1. 提交了session,这个就是之前的session
  2. 提交了 kksj(开课时间)、kcxz(课程性质)、kcmc(课程名称)、xsfs(显示方式),竟然用拼音,太low了

# response:
  1. 返回了200 ok, 也就是下面body里的内容就是最终包含成绩的html源代码了,去解析吧

实现(Nodejs)

我是用了Bmob的后端云,只支持nodejs,所以使用了nodejs。

1. 导依赖模块,定义全局变量

var req = require('request'); // http请求库
var htmlparser = require('htmlparser'); // html解析库

var kksj = '2015-2016-1'; // 开课时间
var username = '8000113237'; // 学号
var password = 'xxxxxxxx'; // 密码

var loginUrl = 'http://218.64.56.18/xk/LoginToXk'; // 登录地址
var scoreUrl = 'http://218.64.56.18/kscj/cjcx_list'; // 成绩地址

2. 模拟登录,获取session

//发起POST请求
req.post({url:loginUrl, form:{USERNAME:username, PASSWORD: password}}, function(err, res, body) {
    console.log(res.headers);
}

// 得到的结果:
F:\Code\nodejs>node getScore.js
{ 
  server: 'Apache-Coyote/1.1',
  'set-cookie': [ 'JSESSIONID=B0C7F4EE40AEA1DE1500F1548B44DC62; Path=/' ],
  location: 'http://218.64.56.18/framework/xsMain.jsp',
  'content-length': '0',
  date: 'Wed, 06 Apr 2016 09:09:11 GMT',
  connection: 'close' 
}
  
// 在http响应header中,得到了cookie里面包含的session,接下来我们再带着这个session去访问成绩页面

3. 抓取成绩页面内容

// 返回的cookie包含了session
var set_cookie = res.headers['set-cookie'];
// 配置请求参数
var options = {
    url : scoreUrl,
    headers: {
        'cookie': set_cookie
    },
    form: {          /* 提交查询条件 */
       'kksj': kksj, 
       'kcxz': '',
       'kcmc': '',
       'xsfs': 'all'
    }
};
// 发送post请求
req.post(options, function(err,res,body){
    console.log(body);
}

// 这里返回的是html源码,比较长,就不展示了,我们的成绩保存在 table 中
// 由于页面只有一个 table,我们可以把table部分解析出来,也可以直接取 td 内容
// 这里使用 htmlparser 库

4. 解析html,获得成绩

我是用的是 htmlparser 库,一个可以通过 id、name、class、type等多种方式匹配的解析库,这个库现在出了 htmlparser2 ,更多去看官方文档吧 htmlparser

// 解析html, body即之前获取到的网页源代码
var rawHtml = body;
var handler = new htmlparser.DefaultHandler(function (err, dom) {
    }, { verbose: false });

var parser = new htmlparser.Parser(handler);
parser.parseComplete(rawHtml);

// 获取姓名
var loginName = htmlparser.DomUtils.getElementById('Top1_divLoginName',handler.dom)
// 将获取到的的集合转化为json输出,这里的2只用两个空格缩进输出
console.log(JSON.stringify(loginName, null, 2));

执行后就获得了下面的内容:

{
  "type": "tag",
  "name": "div",
  "attribs": {
    "id": "Top1_divLoginName",
    "class": "Nsb_top_menu_nc",
    "style": "color: #000000;"
  },
  "children": [
    {
      "data": "汪鹏(8000113237)",
      "type": "text"
    }
  ]
}

到这里,姓名学号就获得到了,直接通过loginName['children']['data']就可获得了。 课程成绩信息也可以通过同样的方法来解析完成!

附:所有源码 getScore.js

/**
 * 抓取强智新教务系统成绩
 * Created by chiyiw on 04/04/16.
 */
var req = require('request'); // http请求库
var htmlparser = require('htmlparser'); // html解析库

var kksj = '2015-2016-1'; // 开课时间
var username = '8000113237'; // 用户名
var password = 'xxxxxxxx'; // 密码

var loginUrl = 'http://218.64.56.18/xk/LoginToXk'; // 登录地址
var scoreUrl = 'http://218.64.56.18/kscj/cjcx_list'; // 成绩地址

//发起POST请求
req.post({url:loginUrl, form:{USERNAME:username, PASSWORD: password}}, function(err, res, body) {

    // 返回的cookie,里面包含了session
    var set_cookie = res.headers['set-cookie'];
    // 配置请求参数
    var options = {
        url : scoreUrl,
        headers: {
            'cookie': set_cookie
        },
        form: {
           'kksj': kksj,
           'kcxz': '',
           'kcmc': '',
           'xsfs': 'all'
        }
    };
    // 发送post请求
    req.post(options, function(err,res,body){

        // 解析html
        var rawHtml = body;
        var handler = new htmlparser.DefaultHandler(function (err, dom) {
            }, { verbose: false });

        var parser = new htmlparser.Parser(handler);
        parser.parseComplete(rawHtml);

        var inputs = htmlparser.DomUtils.getElementsByTagName('input',handler.dom);
        // 如果密码错误会跳转到登录界面,这个界面有多个input标签
        if (inputs.length > 2){
            response.send('{"name":"用户名或密码错误","term":"","score":[]}');
            return;
        }

        // 获取姓名
        var loginName = htmlparser.DomUtils.getElementById('Top1_divLoginName',handler.dom)['children'][0]['data'];

        // 查询到所有的td标签
        var tds = htmlparser.DomUtils.getElementsByTagName('td', handler.dom);
        var datas = htmlparser.DomUtils.getElementsByTagType('text', tds);

        // 获取学期
        var term = datas[1]['data'];

        // 添加成绩到数组中
        var score = new Array();

        for (var i = 0; i < datas.length; i++){
            if (i%10 == 0){
                var group = new Object();
                group.course = datas[i+3]['data'];
                group.score = datas[i+4]['data'];
                group.credit = datas[i+5]['data'];
                group.type = datas[i+9]['data'];

                score.push(group);
            }
        }

        // 输出JSONObject
        var result = new Object();
        result.name = loginName;
        result.term = term;
        result.score = score;

        // 转化为字符串输出
        console.log(JSON.stringify(result,null,2));
    });
});