教务管理系统:成绩、课表查询接口设计及抢课、监控功能实现

秋季学期即将到来,各大高校陆续公布新课表、期末成绩、定时开放抢课,但高校的网络一向是一言难尽。针对没有开放接口的教务系统,如何实时查询课表、成绩,如何获取一手选课系统开放情况、课程剩余量,如何第一时间抢到心仪限选课程,之前我曾在个人博客上用 Java 针对青果和强智科技开发的教务系统实现了成绩查询和抢课功能,本次针对新需求,用Python整活儿,带大家梳理各大教务管理系统功能实现的基本逻辑,抛砖引玉,以便大家举一反三。

温馨提示:请各位遵守学校的各项规定,请勿通过重复发包等操作干扰教务系统的正常运行。

步骤梳理:
  1. 处于登陆状态
  2. 查课表/查成绩/抢课
  3. 收到服务端反馈
  4. 解析数据,提升可读性
  5. 具体应用(抢课;定时监测成绩、课表;获取选课系统开放情况、课程剩余量;API)

其中需要强调的是所有步骤中可变性最大的一环,即第一步。所谓处于登陆状态,应当与登陆教务系统相区别,尽管登陆教务系统其目的在于处于登陆状态,但如果我们发现教务系统可以长期保持登陆状态,那么只需要在编写程式的过程中手动登陆一次教务系统获取其cookie值保存至源码中即可,此后保持cookie值的有效即可而无需每次执行登陆教务系统操作。

而“处于登陆状态”,常常能成功劝退一批人,劝退原因主要有:

  • 其一,针对查询操作带有时间戳的教务系统,难以长期保持登陆状态;
  • 其二,即使能够长期保持登陆状态,受单地登陆影响,如用户手动登陆系统,原cookie值失效,源码失效需要复写;
  • 其三,登陆页面常常设置了多重防护;

对此解决方法也很简单,那就是 对症下药


思路分析:

在这里插入图片描述

首先我们看看今天的教务管理系统(这里就不透露具体是哪家了),分析如何通过验证并登陆,思考路径可以从以下几点入手:

  1. 是否能够使用固定cookie值长期保持登陆状态,以绕过登陆验证页面;
  2. 是否考虑通过selenium 模拟浏览器操作实现动态HTML处理,登陆页面
  3. 是否能够通过模拟post请求通过验证并登陆

经测试,前两种方法在该教务系统的应用上都存在缺陷,且实现方法较为简单,这里为了方便大家举一反三,我就选择第三个方案,使用普遍更为常用的即通过模拟post请求的形式实现登陆需求。

其次,在成功登陆后,同样使用post发包的形式获取(发送)需求中的相关信息(行为)。

最后则是分析数据包,清洗数据,提升可读性,设计交互式io界面,实现课表、期末成绩查询、监测选课开放情况、课程剩余量以及抢课等功能。


一、登陆

(1)分析post请求

模拟登陆抓包后分析请求:

在这里插入图片描述
Flag是包类型,固定值为Loginusernamepassword用户名密码ddlUserClass身份类型,用于数据库匹配,code1ImageButton2.xImageButton2.y验证码相关,此外附带了两个值:__VIEWSTATE__VIEWSTATEGENERATOR

报文分析可知,确定变量有5,分别是 __VIEWSTATE__VIEWSTATEGENERATOR以及 code1ImageButton2.xImageButton2.y

分析页面后不难发现,每次页面载入后, __VIEWSTATE__VIEWSTATEGENERATOR的值将会通过form表单的形式体现在源码中。而多次发包后能够确定ImageButton2.xImageButton2.y的重合值。

基于上述分析,原本复杂的问题就已经迎刃而解了。先梳理清楚操作步骤和目标,并引入相关环境变量:

  1. 获取 __VIEWSTATE__VIEWSTATEGENERATOR的值;
  2. 获取并识别验证码,生成 code1
  3. 拼接 post 包
  4. 发包
  5. 获取服务器状态响应
#!C:\Python34\python3.exe
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import requests
import urllib.request
import urllib.parse
from PIL import Image  # 用于打开图片和对图片处理
import pytesseract  # 用于图片转文字
import re
from bs4 import BeautifulSoup
import json
import time
(1)获取 __VIEWSTATE以及 __VIEWSTATEGENERATOR 的值

前面我们已经发现,__VIEWSTATE__VIEWSTATEGENERATOR的值在每次页面载入后将会通过form表单的形式体现在源码中,那就直接用正则表达式截取内容:

首先,createddate 以及 id 的值只有在登陆后才能获取,因此获取网页源码时需要带上 Cookie 值和必要的请求头。

url = '手动打码/Login.aspx'
def get_hiddenvalue():
        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0",
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
            "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
            "Accept-Encoding": "gzip, deflate",
            "Content-Type": "application/x-www-form-urlencoded",
            "Origin": "手动打码",
            "Connection": "keep-alive",
            "Referer": "手动打码",
            "Upgrade-Insecure-Requests": "1"
        }
        main = session.get(url, headers=headers)
        gb_headers = main.headers

会话对象requests.Session能够跨请求地保持某些参数,比如cookies,即在同一个Session实例发出的所有请求都保持同一个cookies,而requests模块每次会自动处理cookies,这样就很方便地处理登录时的cookies问题。因此发包使用的是 session.get(url, headers=headers),后文同理。

其次,上正则表达式:

        VIEWSTATE = re.findall(r'<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="(.*?)" />', main.text,re.I)
        VIEWSTATEGENERATOR = re.findall(r'input type="hidden" name="__VIEWSTATEGENERATOR" id="__VIEWSTATEGENERATOR" value="(.*?)" />', main.text,re.I)

这里需要注意的是,每次页面刷新后验证码的值也将改变,因此在返回__VIEWSTATE__VIEWSTATEGENERATOR的值同时,还需要返回post请求发送后服务器返回的Set-Cookie值,以便于后续验证码的获取:

        return VIEWSTATE[0], VIEWSTATEGENERATOR[0], gb_headers

#test = get_hiddenvalue()
#print(test[0]) #VIEWSTATE
#print(test[1]) #VIEWSTATEGENERATOR
#print(test[2]) #gb_headers
#print(test[2][“Set-Cookie”]) #gb_headers

(2)获取 code1 的值
  1. 获取并保存验证码:

正如上文说述,每次页面刷新后验证码的值也将改变,因此发包需要用到第一次请求时的cookie值:

url2 = '手动打码/Image.aspx'
def get_pic():
    # 验证码请求头
    headers2 = {
        "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0",
        "cookie": "varPartNewsManage.aspx=10" + test[2]["Set-Cookie"]
    }

    re_pic = requests.get(url2, headers=headers2)
    response = re_pic.content
    #print(url2)
    #print(re_pic.request.headers)
    #request = urllib.session.Request(url=url)

    # request = urllib.request.Request(url=url2,headers=headers2)
    # response = urllib.request.urlopen(request)
    # main = response.read()

    # response = session.get(url2,headers=headers2)
    # print(response.request.headers)
    # main = response.content
    # print(main)
    file = "C:\\Users\\john\\Desktop\\1\\" + ".png"
    playFile = open(file, 'wb')
    playFile.write(response)
    playFile.close()
  1. 识别验证码:
def recognize_captcha(img_path):
    im = Image.open(img_path)
    num = pytesseract.image_to_string(im)
    return num
#print(pic_res) #验证码识别结果
(3)拼接 post 包、发包、收包
def post_login():
    headers3 = {
        "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
        "Accept-Encoding": "gzip, deflate",
        "Content-Type": "application/x-www-form-urlencoded",
        "Origin": "手动打码",
        "Connection": "keep-alive",
        "Referer": "手动打码",
        "Upgrade-Insecure-Requests": "1",
        "cookie": "varPartNewsManage.aspx=10;" + test[2]["Set-Cookie"]
    }
    data = {"__VIEWSTATE": test[0],
            "__VIEWSTATEGENERATOR": test[1],
            "Flag": "Login",
            "username": "手动打码",
            "password": "手动打码",
            "ddlUserClass": "1",
            "code1": pic_res,
            "ImageButton2.x": "64",
            "ImageButton2.y": "10"}
    res = session.post(url=url,data=data,headers=headers3)
    #print(res.request.headers)  #核验cookie是否有效带上
    #print(res.text)

二、查课表、查成绩、抢课等操作

在成功登陆系统后,具体操作就很容易了,分析post请求后自行拼装即可:

(1)查课表、抢课功能实现

针对简单查询或是单纯发送post请求,实现原理就是发包,以查课表为例:

def DisplayCourseTable():
    headers4 = {
        "Host": "手动打码",
        "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
        "Accept-Encoding": "gzip, deflate",
        "Connection": "keep-alive",
        "Referer": "手动打码",
        "Upgrade-Insecure-Requests": "1",
        "cookie": "varPartNewsManage.aspx=10;" + test[2]["Set-Cookie"]
    }
    CourseTable = session.get(url=DisplayCourseTableURL,headers=headers4)
    #print(Score.request.headers)  #核验cookie是否有效带上
    #print(CourseTable.text)
    return CourseTable.text
(2)查成绩功能实现

相比于查课表和抢课,成绩查询有其特殊性,一方面成绩列表随着课程的增加持续变动,另一方面在此教务系统中,post请求提交后服务器响应结果是html,基于此需要通过正则表达式获取所需数值,因此这里展开讲一下查成绩功能实现。

在这里插入图片描述

def Score_TO_Table():
    bs = BeautifulSoup(CourseScoreView(), "html.parser")
    #print(bs.tbody)
    result = []
    allrows = bs.tbody.findAll('tr') #提取表格
    for row in allrows:
        result.append([])
        allcols = row.findAll('td')
        for col in allcols:
            thestrings = [str(s) for s in col.findAll(text=True)]
            thetext = ''.join(thestrings)
            result[-1].append(thetext)
            #print(type(thestrings))
    new_result = [[s.replace(' ', '',) for s in x] for x in result] #去除空格
    print(new_result)

在这里插入图片描述
需要的信息是课程名称以及成绩:

在这里插入图片描述

    succssed_a = []
    succssed_b = []
    #print(len(new_result))
    for i in range(1,len(new_result)-3):      #1开头去第一行<tr>标题,最后三行无数据
        #print("{}: {} ".format(new_result[i][2],new_result[i][9]))
        a = "".join(new_result[i][2].split())  #去除new_result中的 \r\n\xa0
        b = "".join(new_result[i][9].split())
        succssed_a.append(a)
        succssed_b.append(b)
    succssed = dict(zip(succssed_a,succssed_b))  #将列表变成字典
    print(succssed)

在这里插入图片描述
转换成 json 格式:

    jsoninfo = json.dumps(succssed,indent=4,ensure_ascii=False,sort_keys=True,separators=(",",":")) #将字典转为json
    print(jsoninfo)

最后再调整一下格式:

if __name__ == '__main__':
    session = requests.Session()
    test = get_hiddenvalue()
    get_pic()
    pic_res = recognize_captcha("C:\\Users\\john\\Desktop\\1\\" + ".png")
    #print(pic_res)  # 验证码识别结果
    post_login()
    Score_TO_Table()
    input()

输出结果


三、拓展应用

(1)定时监测成绩、课表或选课系统开放情况、课程剩余量等

这类操作主要可以分为两大类,一类是直接发送post请求服务器即可返回结果的,一类是监测是否存在预定数值,当出现后自动通报的。如在选课情境下,无人值守自动补选监听可以写作:

def Lisen():
    time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    headers4 = {
        "Host": "手动打码",
        "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
        "Accept-Encoding": "gzip, deflate",
        "Connection": "keep-alive",
        "Referer": "手动打码",
        "Upgrade-Insecure-Requests": "1",
        "cookie": "varPartNewsManage.aspx=10;" + test[2]["Set-Cookie"]
    }
    CourseTable = session.get(url=DisplayCourseTableURL,headers=headers4)
    str = "监听内容"
    if str in CourseTable.text:
       print("已检测到" + "\n" + time)
       mail()  #邮件提醒
       global num
       num += 1            
    else:
       print("暂无" + "\n" + time)

可以选择通过简单邮件传输协议方式监听结果:

my_sender = '···'  # 发件人邮箱账号
my_pass = '···'  # 发件人邮箱密码
my_user = '···'  # 收件人邮箱账号,我这边发送给自己

def mail():
    ret = True
    try:
        msg = MIMEText('邮件内容:'+resp.text, 'plain', 'utf-8')
        msg['From'] = formataddr(["···", my_sender]) 
        msg['To'] = formataddr(["···", my_user]) 
        msg['Subject'] = "邮件主题:"+resp.text 

        server = smtplib.SMTP_SSL("smtp.qq.com", 465)  
        server.login(my_sender, my_pass)  
        server.sendmail(my_sender, [my_user, ], msg.as_string())  
        server.quit()  
    except Exception: 
        ret = False
    return ret

ret = mail()
if ret:
    print("邮件发送成功")
else:
    print("邮件发送失败")

可以设置监测周期等等:

if __name__ == '__main__':
    schedule.every().day.at("12:00").do(mail)
if __name__ == '__main__':
    while True:
        try:
            Lisen()
            time.sleep(300)
            if num == 6:
                break
        except Exception as err:
            print(err)
(2)制作api开放查询接口

如通过与微信小程序结合等方式拓展其可用性。

在这里插入图片描述

总之,在打通教务管理系统,实现相应功能之后,相关拓展应用就很多了,在此就不一一具体例举。


至此,本文也就进入尾声了。本文的撰写来自于开发中的一点心得体会,基于Python,主要目的是帮大家梳理了在教务系统没有公开接口的情况下,如何实时查询课表、成绩,获取一手选课系统开放情况、课程剩余量,第一时间抢到心仪限选课程,供对这一领域感兴趣的读者以参考借鉴。希望本文能够起到抛砖引玉之效,也欢迎大家的批评交流。


如果您有任何疑问或者好的建议,期待你的留言、评论与关注!

©️2020 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页