模拟登录教务处查询成绩

在我们学校有一个“东大小秘书”的微信应用,可以利用它直接查询成绩,十分方便。于是我也想着自己去实现一个,这并没有很大的难度。关键是需要通过验证码认证。

这里无非就是模拟登录教务处然后抓取数据,之后进行HTML解析即可。比较有难度的验证码破解,考虑到验证码并不是太复杂,可以使用第三方OCR库进行识别。经过对node的这方面的考察之后,发现了一个dv的第三方库,对tesseract进行了封装,使用起来十分方便,唯一缺憾就是目前仅支持到node v0.10版本。

话不多说,下面开始动手。

由于考虑到今后可能提供公众服务,这里手动搭建了一个HTTP服务器。GET的方式进行查询。地址构造如下: /?id=xxx&pw=xxx ,获取到用户名和密码之后直接进入到查询环节。这里查询步骤有:

  1. 获取验证码
  2. 验证码识别
  3. 尝试登录
  4. 获得成绩HTML数据
  5. 解析成绩数据
  6. 返回成绩JSON数据

为了控制程序异步流程,这里我使用了Promise(Bluebird)来组织代码。查询模块代码如下:

    module.exports = function(id, pw) {
        var defer = Promise.defer();
        function init() {
            getVerifyCode(id, pw)
                .then(login)
                .then(fetchScore)
                .then(HTMLParser)
                .then(function(json) {
                    defer.resolve(json);
                })
                .catch(function(err) {
                    if(err === 'CODE-ERROR') {
                        init();    // 错误递归
                    } else if(err === 'LOGIN-ERROR') {
                        init();     // 同上
                    } else if(err === 'PW-ERROR') {
                        defer.reject(JSON.stringify('password_error'));
                    }
                });
        }
        init();
        return defer.promise;
    }

查询过程可能存在 验证码错误登录失败, 密码错误等错误。如果遇到前两种错误则递归重新进行查询,密码错误则返回错误信息给用户。

获取并解析验证码

我们知道验证码的原理就是服务器随机生成字符串然后返回给用户并将验证码写入服务器session中,用户输入之后与服务器session中的字符串进行验证从而判断是否正确。而session的原理是根据用户请求时夹带的cookie进行用户的甄别。所以我们利用cookie即可完成登录的操作。获取验证码的同时需要获取服务器返回的cookie值,作为用户的唯一标识。

    var getVerifyCode = function(id ,pw) {
        return new Promise(function(resolve, reject) {
            var cookie = '';
            var options = {
                host:"202.118.31.197",  
                path:"/ACTIONVALIDATERANDOMPICTURE.APPPROCESS",  
                method:"get",
                headers:{  
                    "Content-Length":contents.length,          
                    "Cookie": cookie
                }
            };

            var req = http.request(options,function(res){
                var chunks = [];
                cookie = res.headers["set-cookie"][0];     // 获取并存储cookie, 也可以写入缓存,加速查询。
                res.on("data",function(chunk){
                    chunks.push(chunk);
                });
                res.on('end', function() {
                    var imgBuff = Buffer.concat(chunks);
                    // 利用dv解析验证码
                    var image = new dv.Image('jpg', imgBuff);
                    var tesseract = new dv.Tesseract('eng', image);
                    var verifyCode = tesseract.findText('plain').trim();
                    var len = verifyCode.length;
                    if(len !== 4) {
                        reject('CODE-ERROR');    // 验证码明显有错误
                    } else {
                        var obj = {
                                   id: id,
                                   pw: pw,
                                   cookie: cookie,
                                   verifyCode: verifyCode
                               };
                        resolve(obj);    // 验证码可能正确,先到下一步去
                    }
                });
            });

            req.write(contents);
            req.end();

        });
    }

以上利用了http.request模块对服务器发起GET请求,成功获取验证码并尝试解析它。以上代码需要注意一个Buffer的处理。不要对Buffer进行相加,正确的做法应该是将每一块数据放入数组中,最后合并数组得到Buffer完整数据。

尝试登录

利用上一步得到的cookie和验证码字符串尝试进行用户登录。使用POST请求,在headers内夹带cookie字段,然后构造一个请求体,然后请求。之后使用iconv-lite对文档进行解码。

    res.on('end', function() {
        var decodedBody = iconv.decode(Buffer.concat(chunk), 'gbk');
        if(decodedBody.indexOf('script') > 0) {
            // getCode();
            // return;
            if(decodedBody.indexOf('您的密码不正确')>0) {
                reject('PW-ERROR');
            } else {
                reject('LOGIN-ERROR');
            }
        } else {
            resolve(cookie);    // 传cookie给下一步
        }
    });

解码之后需要判断一下错误,可能存在上一环节的验证码错误或者用户的密码错误。如果没有错误则进行下一步。这里需要将cookie传入下一步,这个时候使用该cookie就可以进行后台操作了。

获取成绩页面数据

到了这一步就很简单了,夹带cookie,GET请求成绩页面的数据。进行解码,然后传入HTMLParser()

解析HTML数据

上一步我们得到了成绩页面的HTML数据,我们需要对代码进行处理从而得到我们需要的成绩信息。node中有一个cheerio库,可以像jQuery一样操作HTML DOM。这里感觉自己用的不是很恰当,感觉过程有点麻烦。看官稍微看看就行,这个也很容易使用。

    var HTMLParser = function(html) {
        return new Promise(function(resolve, reject) {
            $ = cheerio.load(html);
            var score = $('.color-rowNext');
            var ascore = $('.color-row');
            var ret = [];
            for (var i=0; i < score.length; i++) {
                var course = '';
                var mark = '';
                if(! (score[i]['children'][16]['children'] instanceof Array)) break;
                course = score[i]['children'][5]['children'][0]['data'].trim();
                mark = score[i]['children'][16]['children'][0]['data'].trim();
                ret.push({
                    course: course,
                    score: mark
                });
            }
            for (var i=0; i < ascore.length; i++) {
                var course = '';
                var mark = '';
                if(! (ascore[i]['children'][16]['children'] instanceof Array)) break;
                course = ascore[i]['children'][5]['children'][0]['data'].trim();
                mark = ascore[i]['children'][16]['children'][0]['data'];
                ret.push({
                    course: course,
                    score: mark
                });
            }
            resolve(JSON.stringify(ret));
        });
    }

到这里我们就成功的获取到了成绩啦。在操作的过程中需要对错误进行处理,使得用户能一次性得到准确的成绩数据。

最后,我们把数据模块封装,给服务器模块调用即可。我把代码放到Github上了,有兴趣的可以看看。如有错误,欢迎指出。

=>程序源码