用Python脚本实现刷课

微信内部浏览器查看代码存在兼容性问题,建议在微信外打开。

本文使用Chrome浏览器,所有代码在Python 3.5.1下正常执行。
用按键精灵更简单,可以咨询会用的同学
此文章及代码仅供学习⚣交流使用,请自觉遵守学校规定,刷课后果自负/滑稽。

02.25日更新 续

基本原理

自动选课实现原理

登录选课网,进入退补选界面(补选退选)。在可以补选的课程的补选按钮上点击右键->检查,可以看到,这实际上是一个链接。

<a href="/elective2008/edu/pku/stu/elective/controller/supplement/electSupplement.do?index=0&amp;seq=BKC00132351AT0000241" style="width: 30" onclick="return confirmSelect('几何学习题','1','','0','BKC00132351AT0000241');"><span>补选</span></a>  

可以看到,在点击补选链接时,系统先调用confirmSelect函数弹出确认窗口,用户确认后,进入链接:

http://elective.pku.edu.cn/elective2008/edu/pku/stu/elective/controller/supplement/electSupplement.do?index=0&seq=BKC00132351AT0000241

其中关键的后台组件是electSupplement.do,向其传送了两个参数indexseq,猜想后台此时验证网页上验证码框的内容,如果验证成功即可选上。

自动查询选课人数实现原理

同上,不能补选的课会有刷新按钮,例如社会性别研究,在其刷新上点击右键->检查,其代码如下:

<a id="refreshLimit22" href="javascript:void(0)" style="width: 30" onclick="refreshLimit('社会性别研究','1','','2','BKC03130280AT0006152','150');"><span>刷新</span></a>  

可以看到,点击刷新时,调用了javascript函数refreshLimit(),查看其定义为:

function refreshLimit(courseName,classNo,cancelTimeMsg,index,seqNo,limitedNbr) {  
            var now = new Date();
            clearMsg(); // 清除提示信息
            dif = now.getTime()-refreshTime.getTime();
            if(dif /1000 < 5){
                alert("对不起,您需要5秒之后才可以刷新。");
                return;
                }

                refreshIndex = index;
                limitedNum = parseInt(limitedNbr);
                refreshCourseName = courseName;
                refreshClassNo = classNo;
                refreshCancelTimeMsg = cancelTimeMsg; 
                refreshSeqNo = seqNo;

                if (xmlHttp == null) {
                createXMLHttpRequest();
            }
             var url = "/elective2008/edu/pku/stu/elective/controller/supplement/refreshLimit.do?index=" + index+"&seq="+seqNo;
             xmlHttp.open("GET", url, true);
             xmlHttp.onreadystatechange = refreshCallback;
             xmlHttp.send(null);
        }

可以开心地看到,前文需要的indexseq就是这个函数的第4、5个参数,而且五秒的刷新频率是在这里被设定的,我们可以绕过它。继续阅读还可以发现,当前选课人数是通过向refreshLimit.doindexseq两个参数实现的,而且返回的是一个xml文档,以前文的“社会性别研究”为例,其index=2seq=BKC03130280AT0006152,故其xml文档的地址为:

http://elective.pku.edu.cn/elective2008/edu/pku/stu/elective/controller/supplement/refreshLimit.do?index=2&seq=BKC03130280AT0006152

打开这个地址,我们得到这样一个xml文件:

<numResponse>  
    <electedNum>150</electedNum>
</numResponse>  

其中的150显然就是当前的已选人数。

这样我们就得到了所有需要的内容,刷课完事就绪,只差代码了。

分步代码实现

为了方便地抓取网页,解析xml,实现发邮件、发出声音等多种高级功能。我们使用简洁的Python (3)作为编程语言。

我们使用url.request包中的build_opener()函数得到一个OpenerDirector对象,其用于附加Cookie,向elective请求数据。

import urllib.request;

opener = urllib.request.build_opener();  
opener.addheaders.append(('Cookie','JSESSIONID=' + cookieID));  

其中cookieID为当前会话的cookie值,可以用Chrome的开发工具方便的获取。

先获取选课人数,使用opener发送参数请求refreshLimit.do,得到xml文件,为了方便地解析xml,我们需要导入xml.dom.minidom

import  xml.dom.minidom;

arg = '?index=' + myIndex + '&seq=' + myCourSeq;  
renshu = 'http://elective.pku.edu.cn/elective2008/edu/pku/stu/elective/controller/supplement/refreshLimit.do' + arg;  
ele = opener.open(renshu).read;  
c = str(ele, encoding = 'utf8');  
root = xml.dom.minidom.parseString(c);  
electedCount = str(root.getElementsByTagName('electedNum')[0].firstChild.nodeValue);  

便可得到当前已选人数,这个方法比直接在网页前端刷新要更快,因为理论上不存在5秒刷新的限制(实际上内部服务器还是存在一定限制,刷新频率可以设在3秒左右。

检测到已选人数不等于满员时,理论上便可以传参数给electSupplement.do,进行自动选课:

arg = '?index=' + myIndex + '&seq=' + myCourSeq;  
buxuan = 'http://elective.pku.edu.cn/elective2008/edu/pku/stu/elective/controller/supplement/electSupplement.do' + arg;  
ele = opener.open(buxuan);  

而实际上服务器此时可能会严格检查会话是否超时、验证码是否超时等,会导致不一定能自动选课成功,详细情况待研究。但我们可以通过邮件、发声等方法通知用户当前可选,让用户手动选课。

基本原理便如上所述,下面提供一份现成的可用代码供大家交流学习。

可用代码

(请原谅我C一样的代码风格)

#shuake.py
#By Jet 2016

import urllib.request;  
from urllib.error import HTTPError, URLError;  
import  xml.dom.minidom;  
from time import sleep;  
import smtplib;  
from email.mime.text import MIMEText;

#Data Needed Filling
myIndex = '***myIndex***'; #填课程索引号  
myCourMax = '***myCourMax***'; #填课满人数  
myCourSeq = '***myCourSeq***'; #填课程序列号  
cookieID = '***cookieID***'; #填你当前的cookie值

#Your QQMail
#可选,填写后,若可选或出错会自动发邮件
user = "***QQMailID***";  
pwd  = '***QQMailPswd***';



arg = '?index=' + myIndex + '&seq=' + myCourSeq;  
renshu = 'http://elective.pku.edu.cn/elective2008/edu/pku/stu/elective/controller/supplement/refreshLimit.do' + arg;  
buxuan = 'http://elective.pku.edu.cn/elective2008/edu/pku/stu/elective/controller/supplement/electSupplement.do' + arg;  
opener = urllib.request.build_opener();  
opener.addheaders.append(('Cookie','JSESSIONID=' + cookieID));  
errCnt = 0;  
timeOut = 3;  
ret = myCourMax;  
timeCnt = 0;

def refresh():  
    global timeCnt, errCnt;
    timeCnt += 1;
    print('Still Trying... 当前剩余人数 = ' + ret + ' 总尝试次数 = ' + str(timeCnt) + ' 错误次数 = ' + str(errCnt));
    try:
        ele = opener.open(renshu).read();
        c = str(ele, encoding = 'utf8');
        root = xml.dom.minidom.parseString(c);
        return  str(root.getElementsByTagName('electedNum')[0].firstChild.nodeValue);    
    except HTTPError as e:
        if (e.code == 500):
            refresh();
        else:
            errCnt += 1;
        return 'HTTP ERROR ' + str(e.code);
    except URLError as f:
        return 'URL ERROR ' + str(f.errno);
    except:
        return 'UNKNOW ERROR';

def xuan():  
    print('正在尝试自动选课!!!');
    try:
        ele = opener.open(buxuan);
        print('Successfully BuXuan!');
        mail('课可以选了!!!快去看看自动选有没有成功!!');
        while (1):
            Beep(1500, 1000);
    except:
        while (1):
            Beep(1500, 1000);


def mail(m):  
    msg = MIMEText(m);
    msg["Subject"] = m;
    msg["From"] = user;
    msg["To"] = user;

    s = smtplib.SMTP_SSL("smtp.qq.com", timeout = 30);
    s.login(user, pwd);
    s.sendmail(user, user, msg.as_string());
    s.close();


while (1):  
    sleep(timeOut);
    ret = refresh();
    if (ret == myCourMax):
        errCnt = 0;
    elif (ret == str(eval(myCourMax + '-1'))):
        xuan();
        break;
    elif (ret[0] != 'H'):
        errCnt += 1;
    if (errCnt >= 15):
        print('Unknown Error');
        mail("发生了错误:\n" + ret);
        break;

该脚本考虑了简单的错误处理,尤其是HTTP 500错误在elective服务器上是常见的,该错误被忽略。 其中要填的部分为:

#Data Needed Filling
myIndex = '***myIndex***'; #填课程索引号  
myCourMax = '***myCourMax***'; #填课满人数  
myCourSeq = '***myCourSeq***'; #填课程序列号  
cookieID = '***cookieID***'; #填你当前的cookie值

#Your QQMail
#可选,填写后,若可选或出错会自动发邮件
user = "***QQMailID***";  
pwd  = '***QQMailPswd***';  

这些参数要如何获取呢?在你要补选的课程的刷新按钮上点右键->检查,refreshLimit()函数的倒数第三个参数是myIndex,倒数第二个参数是myCourSeq,最后一个参数是myCourMax。例如下图: fig1 我们可以得到:

myIndex = '2';  
myCourMax = '150';  
myCourSeq = 'BKC03130280AT0006152';  

Cookie也很好获取,在当前网页上按F12,切换到Resources页,找到Cookies -> elective.pku.edu.cn,其中JSESSIONID一栏的值就是cookieID,如下图:
fig2

其中5Jh....开头的那一段就是cookieID。(注意,这一栏很长,而且带有连字符,容易少复制)我这次会话的cookieID为:

cookieID = '5JhYWNjHLfyh59GkmNzGYyh8JCtGL2Q8FQlTQ8ZGcG293zwhvkK2!-501355683!-929504223'  

如果比这个短很多,说明复制漏了。

之后的QQ邮箱账号密码可填可不填,用于发通知邮件。

该脚本使用方法

先打开elective,登录,切换到补退选页面,按上述方法填好脚本参数,填好页面上的验证码,运行脚本即可。网页别关,脚本窗口别关,就可以去做别的事啦,可选时便会尝试自动选课并响铃提示,这时你应该手动刷新elective检查是否自动选课成功,若失败便可以手动选课。

如果脚本遇到无法处理的意外退出,再次运行时只需要重新填写cookieID、页面上的验证码即可,课程索引等相关信息是不会改变的。

认真部分结束


有人说,你这个脚本这么吼,是不是该闷声大发财啊,万一被教务发现了明年没法用了怎么办啊。

其实,我觉得这个系统这么辣鸡,迟早得改革啦,而且这种脚本的原理是十分简单的,年年都有人用啊,系统太naive,当然得升级啊,是不是。

我只想说,如果真的系统升级,再也不挂、不卡、不掉的话,那真是: fig3

Jet

继续阅读此作者的更多文章

北京市 海淀区