SUSTech 研究生 GPA 计算

利用 tis 的接口查询培养方案及成绩,并根据换算表计算 GPA。

等级 A+ / A A- B+ B B- C+ C C- D+ D F
绩点 4.0 3.7 3.3 3.0 2.7 2.3 2.0 1.7 1.3 1.0 0
百分 参考 95~ 100 90~ 94 85~ 89 80~ 84 77~ 79 73~ 76 70~ 72 67~ 69 63~ 66 60~ 62 <60

安装依赖:pip install beautifulsoup4 requests pandas

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import re
import typing as t

import bs4
import pandas as pd
import requests


Cookies = t.Dict[str, str]
Grades = t.List[t.Dict[str, t.Any]]


class TIS:
    def __init__(self, cookies: Cookies) -> None:
        self._cookies = cookies
        self._grades = None
        self._rank = None

    @property
    def grades(self) -> pd.DataFrame:
        if self._grades is None:
            self._grades = self._get_grades(self._get_fah())
        keys = {
            'kcdm': '课程代码', 'kcmc': '课程名称', 'kzmc': '课组名称', 'kkyxmc': '开课学院',
            'xszscj': '课程成绩', 'xscj': '课程等级', 'xf': '学分',
        }
        df = pd.DataFrame([
            tuple(grade[key] for key in keys) for grade in self._grades
        ])
        df.columns = pd.MultiIndex.from_tuples(keys.items())
        return df

    @property
    def gpa(self) -> float:
        grade_credit = [
            (int(row['xszscj'][0]), float(row['xf'][0]))
            for _, row in self.grades.iterrows()
            if row['xszscj'][0] is not None and row['xscj'][0] != 'P'
        ]
        credits = sum(c for _, c in grade_credit)
        if credits == 0:
            return 0.0
        return sum([self._mapper(g)*c for g, c in grade_credit]) / credits

    @property
    def sa(self) -> float:
        '''Score Average'''
        grade_credit = [
            (int(row['xszscj'][0]), float(row['xf'][0]))
            for _, row in self.grades.iterrows()
            if row['xszscj'][0] is not None
        ]
        credits = sum(c for _, c in grade_credit)
        if credits == 0:
            return 0.0
        return sum([g*c for g, c in grade_credit]) / credits

    @property
    def rank(self) -> str:
        '''Rank'''
        if self._rank is None:
            url = 'https://tis.sustech.edu.cn/cjgl/xscjgl/xsgrcjcx/queryXnAndXqXfj'
            headers = {'RoleCode': '02'}
            response = requests.post(url, headers=headers, cookies=self._cookies)
            self._rank = response.json()['xfjandpm']['PM']
        return self._rank

    @classmethod
    def login(cls, username: str, password: str) -> 'TIS':
        url = 'https://cas.sustech.edu.cn/cas/login'
        params = {'service': 'https://tis.sustech.edu.cn/cas'}
        cookies = requests.get('https://tis.sustech.edu.cn').cookies
        with requests.session() as session:
            response = session.get(url, params=params, cookies=cookies)
            soup = bs4.BeautifulSoup(response.content, 'html.parser')
            execution = soup.select('input[name$="execution"]')[0]['value']
            data = {
                'username': username, 'password': password,
                'execution': execution, '_eventId': 'submit',
            }
            session.post(url, params=params, data=data, cookies=cookies)
        return cls(dict(cookies))

    def _get_fah(self) -> str:
        url = 'https://tis.sustech.edu.cn/xspyyjsfasq/grjhzd/2'
        response = requests.get(url, cookies=self._cookies)
        pattern = re.compile(r'(?<=var param_fah = \')[a-zA-Z0-9]+?(?=\')')
        return next(pattern.finditer(response.text)).group()

    def _get_grades(self, fah: str) -> Grades:
        url = 'https://tis.sustech.edu.cn/xspyyjsfasq/queryFakcPage'
        data = {
            'fah': fah, 'loading': 'true', 'pylx': '2', 'isEdit': '1',
            'multiple': 'false', 'labelyxwidth': '100', 'cksf': '1', 'sffaw': '0',
        }
        response = requests.post(url, cookies=self._cookies, data=data)
        return response.json()['list']

    def _mapper(self, grade: int) -> float:
        for (boundary, value) in [
            # 研究生
            (95, 4.0), (90, 3.7), (85, 3.3), (80, 3.0), (77, 2.7),
            (73, 2.3), (70, 2.0), (67, 1.7), (63, 1.3), (60, 1.0),
            # 本科生
            # (97, 4.00), (93, 3.94), (90, 3.85), (87, 3.73), (83, 3.55),
            # (80, 3.32), (77, 3.09), (73, 2.78), (70, 2.42), (67, 2.08),
            # (63, 1.63), (60, 1.15),
        ]:
            if grade >= boundary:
                return value
        return 0.0


if __name__ == '__main__':
    tis = TIS.login(input('(username) >>> '), input('(password) >>> '))
    print(f'Rank: {tis.rank}')
    print(f'GPA:  {tis.gpa:.2f}')
    print(f'SA:   {tis.sa:.2f}')
    print(
        tis.grades
            .droplevel(0, axis=1)
            .sort_values(by=['课程成绩', '学分'], ascending=False)
            .to_markdown(index=False)
    )