2018.3.7 Update

服务器版

分析

http://elite.nju.edu.cn/jiaowu/student/elective/readRenewCourseList.do?type=1

选课页面
观察课程补选的网址我们发现整个选课程序应该是依赖于一个CourseList.do的文件,后面的type应该是几门不同分类的经典阅读。选课时点击选择就可以直接选课。

因此主要有两个思路:

  • 1.用chrome的Tampermonkey插件,把表单中已选和限额两个数字提取出来,然后决定是否是否点击选择。
  • 2.用chrome控制台拦截下选课时对服务器的请求,用Python不断发送请求

Tampermonkey尝试 (Failed)

简介

Tampermonkey是一个基于各大浏览器的插件,主要是读取web上的元素,并执行一些操作,例如:改变元素css属性,新增元素,或者进行点击、刷新等操作。

开启debugger模式

debugger是Tampermonkey脚本里面的一条命令,和断点作用相同,开启方式 在此不再赘述

提取元素

选课表格

打开chrome控制台,找到限额、已选、选择的element:

HTML元素分析

从中我们可以看到,整个表格的id为tbCourseList,其中每一行为一个tr,行中每一个元素为一个td
对应js脚本为:

// ==UserScript==
// @name         New Userscript
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       You
// @match        http://elite.nju.edu.cn/jiaowu/student/elective/readRenewCourseList.do?type=2
// @grant        none
//加入JQuery
// @require    http://code.jquery.com/jquery-1.11.0.min.js
// ==/UserScript==
(function() {
    'use strict';
    debugger;
        //对tbCourseList中每个tr元素执行操作
    $('table tbCourseList').find("tbody").find("tr").each(function(){
        debugger;
        //tr的第4个元素为为课程限额人数
        var courseNum = $(this).children("td").eq(4).text();
        //tr的第5个元素为已选人数
        var selecNum = $(this).children("td").eq(5).text();
        if(courseNum>selecNum){
            console.log(selecNum);
            }
        else{
            console.log("hello");
            }
        }
 );
})();

报错

但是实际执行的时候出现了错误:
无法刷新出表单
也就是说表单并没有加载出来就执行了脚本,所以无法找到table元素,关闭脚本刷新后刷新页面:
jw选课加载时间图

发现负责加载选课列表的coureseList.do是在800ms之后加载的,因此需要保证js脚本在800ms之后执行,因此在开头加入检测tbCourseList是否加载完成的函数:

$("tbCourseList").on("load", function () {
    console.log("loadFinished");   
});

但是实际使用中发现并没有起效,依旧在tbCourseList加载出来之前执行了脚本,tampermonkey尝试以失败告终

想法

其实主要还是对JS语法不熟悉(感觉JS语法和之前用过的所有语言差别太大了……),如果有熟悉JS语法的朋友欢迎与我交流
写这篇Blog的时候又有一个想法,既然在800ms之后加载,那能不能直接在开头写一个延时2s的函数?不过既然已经拿python写完了,我也实在无心再尝试了。【但是不能否认这种方法会比python安全,走前段的话无论url,验证等等怎么变,只要学生能选课,这种方式就能抢课

python尝试(Succeed)

流程分析

整个选课流程主要分为三步:
1.登录教务网(有验证码)
2.进入选课列表
3.递交选课请求
向对应的python的程序流程应该为:
1.获取验证码图片,人工/CV识别之后递交验证码,保留成功cookie
2.用成功的cookie登录教务,并转到选课页面
3.chrome拦截选课请求,然后用python requests模拟发送请求

获取信息

在教务登录页面中用控制台拦截到验证码地址为:

http://elite.nju.edu.cn/jiaowu/ValidateCode.jsp

验证码

拦截到登录请求为:
jw登录
(data中为学号,密码,验证码)

拦截到选课请求为:
选课请求
(data中为课程号,类型)

程序

个人觉得requests是接触到的python最伟大的一个库,以后urlib之类的库应该废掉了,不多说,上代码:

import requests
import pytesseract
import Image
import time
# -*- coding: utf-8 -*-
#验证码自动识别函数(二值法),准确率有待提高,暂不使用
# def varify():
#     img = Image.open('Code.png')
#     pix = img.load()
#     for x in range(img.size[0]):
#         pix[x, 0] = pix[x, img.size[1] - 1] = (255, 255, 255, 255)
#     for y in range(img.size[1]):
#         pix[0, y] = pix[img.size[0] - 1, y] = (255, 255, 255, 255)
#     for y in range(img.size[1]):
#         for x in range(img.size[0]):
#             if pix[x, y][0] < 95 or pix[x, y][1] < 95 or pix[x, y][2] < 95:
#                 pix[x, y] = (0, 0, 0, 255)
#             else:
#                 pix[x, y] = (255, 255, 255, 255)
#     # img.convert('RGB')
#     img.save("temp.jpg",format("jpeg"))
#     text = pytesseract.image_to_string(Image.open('temp.jpg'))
#     if len(text) == 4:
#         text.replace('l', '1')
#         return text
#     else:
#         return raw_input()

codeUrl = 'http://elite.nju.edu.cn/jiaowu/ValidateCode.jsp'
code = requests.get(url=codeUrl)
cookies = code.cookies#保存验证码cookies
f = open('Code.png', 'wb')
f.write(code.content)#按照content(二进制)写入
f.close()

codeNum = raw_input()#手动打开Code.png输入验证码

jw = requests.session()#开始登录教务
data = {'userName': "学号",
        'password': '密码什么的怎么可能给你们',
        'returnUrl': 'null',
        'ValidateCode': codeNum}
jwRes = jw.post("http://elite.nju.edu.cn/jiaowu/login.do", data=data, cookies=cookies)

#假装进入读取课程列表,其实去了这步大概也可以?
readCourse = requests.post("http://elite.nju.edu.cn/jiaowu/student/elective/readRenewCourseList.do?type=2", cookies=cookies)
print readCourse.status_code

#开始构建选课请求
a = requests.session()
#我怀疑其实都不需要header……
a.headers = {
    'Accept': 'text/javascript, text/html, application/xml, text/xml, */*',
    'Accept-Encoding': 'gzip, deflate',
    'Accept-Language': 'zh-CN,zh;q=0.8',
    'Connection': 'keep-alive',
    'Content-Length': '36',
    'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
    'Host': 'elite.nju.edu.cn',
    'Origin': 'http://elite.nju.edu.cn',
    'Referer': 'http://elite.nju.edu.cn/jiaowu/student/elective/readRenewCourseList.do?type=1',
    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36',
    'X-Prototype-Version': '1.5.1',
    'X-Requested-With': 'XMLHttpRequest'
}
#data包含抢课的classid,type,method
data0={
'method':'readRenewCourseSelect',
'classid':78446,
'type':1
}

data1={
'method':'readRenewCourseSelect',
'classid':78448,
'type':1
}

data2={
'method':'readRenewCourseSelect',
'classid':78452,
'type':1
}

data3={
'method':'readRenewCourseSelect',
'classid':78453,
'type':1
}

data4={
'method':'readRenewCourseSelect',
'classid':78479,
'type':4
}

data5={
'method':'readRenewCourseSelect',
'classid':78480,
'type':4
}

i = 0#计算选课次数

while(1):
    t = a.post("http://elite.nju.edu.cn/jiaowu/student/elective/courseList.do", cookies=cookies,data=data0)
    time.sleep(0.02)
    print t.status_code
    t = a.post("http://elite.nju.edu.cn/jiaowu/student/elective/courseList.do", cookies=cookies, data=data1)
    time.sleep(0.02)
    t = a.post("http://elite.nju.edu.cn/jiaowu/student/elective/courseList.do", cookies=cookies, data=data2)
    time.sleep(0.02)
    t = a.post("http://elite.nju.edu.cn/jiaowu/student/elective/courseList.do", cookies=cookies, data=data3)
    time.sleep(0.02)
    t = a.post("http://elite.nju.edu.cn/jiaowu/student/elective /courseList.do", cookies=cookies, data=data4)
    time.sleep(0.02)
    t = a.post("http://elite.nju.edu.cn/jiaowu/student/elective/courseList.do", cookies=cookies, data=data5)
    #理所应当的,jw没对请求频率有任何限制,加入time.sleep只是我怕jw Log崩了【选课请求这种事大概是不需要log的?
    time.sleep(0.3)
    i=i+1
    print t.text#教训,还是应该看一下抢课response到底是要求重新登录,还是该课程已满

总结

目前整个教务系统都采用明文传递,验证码其实可有可无,所有依赖登录的服务都可以让用户自己填写来解决。  
偶然发现NJU小助手不用验证码就获取成绩信息,比较好奇是怎么做到的,有时间的话想拦包看一眼,最近有时间的话会把抢课程序整理一下,部署到web,没时间的话就等下学期吧。
很多时候,很多事情都会背离最初的初衷,就像写这个程序一样,实际上我没有靠它就选到了想要的课,所以写下这篇Blog更多的可能是满足自己的成就感以及破败的虚荣心吧。
最后感谢python,去tmd JS


Time and Tide wait for no man.