├── requirements.txt ├── screenshot ├── notification.jpg └── dingtalkrobot.jpg ├── .gitignore ├── zjusess.py ├── README_CN.md ├── scorenotification.py ├── README.md └── zjuscore.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | colorama -------------------------------------------------------------------------------- /screenshot/notification.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeiPei233/ZJUScoreAssistant/HEAD/screenshot/notification.jpg -------------------------------------------------------------------------------- /screenshot/dingtalkrobot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeiPei233/ZJUScoreAssistant/HEAD/screenshot/dingtalkrobot.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | database.json 2 | userscore.json 3 | dingscore.json 4 | .vscode 5 | __pycache__ 6 | build 7 | dist 8 | zjuscore.spec 9 | test* 10 | *.pkl -------------------------------------------------------------------------------- /zjusess.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import re 3 | import math 4 | 5 | class zjusess(requests.Session): 6 | def __rsa_no_padding__(self, src, modulus, exponent): 7 | m = int(modulus, 16) 8 | e = int(exponent, 16) 9 | t = bytes(src, 'ascii') 10 | # 字符串转换为bytes 11 | input_nr = int.from_bytes(t, byteorder='big') 12 | # 将字节转化成int型数字,如果没有标明进制,看做ascii码值 13 | crypt_nr = pow(input_nr, e, m) 14 | # 计算x的y次方,如果z在存在,则再对结果进行取模,其结果等效于pow(x,y) %z 15 | length = math.ceil(m.bit_length() / 8) 16 | # 取模数的比特长度(二进制长度),除以8将比特转为字节 17 | crypt_data = crypt_nr.to_bytes(length, byteorder='big') 18 | # 将密文转换为bytes存储(8字节),返回hex(16字节) 19 | return crypt_data.hex() 20 | 21 | def login(self, username, password): 22 | 23 | # 打开网站 24 | res = self.get(r'https://zjuam.zju.edu.cn/cas/login') 25 | # 获取execution的值以用于登录 26 | execution = re.findall(r'', res.text)[0] 27 | # 获取RSA公钥 28 | res = self.get('https://zjuam.zju.edu.cn/cas/v2/getPubKey') 29 | modulus = res.json()['modulus'] 30 | exponent = res.json()['exponent'] 31 | 32 | rsapwd = self.__rsa_no_padding__(password, modulus, exponent) 33 | 34 | data = { 35 | 'username': username, 36 | 'password': rsapwd, 37 | 'execution': execution, 38 | '_eventId': 'submit' 39 | } 40 | # 登录 41 | res = self.post(r'https://zjuam.zju.edu.cn/cas/login', data, allow_redirects=False) 42 | if res.text.find('统一身份认证平台') == -1: 43 | return True 44 | else: 45 | return False -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # ZJUScoreAssistant 2 | 3 | 这是一个非常 naive 的命令行成绩助手,用于成绩查询、GPA 计算、成绩更新通知等等。 4 | 5 | ## 运行 6 | 7 | 从仓库中下载 zip 压缩包并解压或 git 该仓库到自己的电脑上。使用 `python zjuscore.py -h` 即可查看帮助。 8 | 9 | ## 使用说明 10 | 11 | 我们可以通过以下参数使用成绩助手: 12 | 13 | ```powershell 14 | python zjuscore.py [-h] [-i username password] [-u] [-ls [YEAR [SEMESTER ...]]] [-n NAME [NAME ...]] 15 | [-g [YEAR [SEMESTER ...]]] [-d [DingWebhook]] [-dn] 16 | ``` 17 | 18 | ### 获取帮助 19 | 20 | 使用 `-h` 或 `--help` 参数即可获取帮助。 21 | 22 | ```powershell 23 | PS > python zjuscore.py -h 24 | usage: zjuscore.py [-h] [-i username password] [-u] [-ls [YEAR [SEMESTER ...]]] [-n NAME [NAME ...]] 25 | [-g [YEAR [SEMESTER ...]]] [-d [DingWebhook]] [-dn] 26 | 27 | ZJU Score Assistant 28 | 29 | options: 30 | -h, --help show this help message and exit 31 | -i username password, --initial username password 32 | initialize your information 33 | -u, --update update the course score 34 | -ls [YEAR [SEMESTER ...]], --list [YEAR [SEMESTER ...]] 35 | list the course and score in a certain year/semester 36 | -n NAME [NAME ...], --name NAME [NAME ...] 37 | search score by the name of the course 38 | -g [YEAR [SEMESTER ...]], --gpa [YEAR [SEMESTER ...]] 39 | calculator the gpa 40 | -d [DingWebhook], --ding [DingWebhook] 41 | set your DingTalk Robot Webhook. Empty means disabled 42 | -dn, --dnotification enable dingtalk score notification 43 | ``` 44 | 45 | ### 初始化成绩助手 46 | 47 | 首先我们需要让成绩助手知道我们的浙大统一身份认证平台的账号密码,才能获取到成绩信息。因此,我们需要通过 `-i` 或 `--initial` 参数来初始化,其后需要跟上两个参数 `用户名(学号)` 和 `密码`。程序在初始化时会自动校验您的用户名和密码。 48 | 49 | ```powershell 50 | PS > python zjuscore.py -i 1234 56 51 | Invalid username or password. Please check them again and use -i to reset them. 52 | 53 | PS > python zjuscore.py -i 320010**** ********** 54 | Initial Success! 55 | ``` 56 | 57 | ### 更新成绩 58 | 59 | 我们可以通过参数 `-u` 或 `--update` 来更新储存在当前电脑上的成绩。这是为了避免每次查询成绩都必须重新查询而浪费时间的情况。 60 | 61 | ```powershell 62 | PS > python zjuscore.py -u 63 | Updated Success! 64 | ``` 65 | 66 | ### 成绩查询 67 | 68 | 通过 `-ls` 或 `--list` 参数来查询成绩。成绩查询支持以下三种使用方式: 69 | 70 | - `python zjuscore.py -ls` 可查询本人的所有的成绩。 71 | 72 | - `python zjuscore.py -ls <学年>` 可查询某一学年的成绩。您可将 `<学年>` 替换为 `2021` 或 `2021-2022` 来查询 2021-2022 学年的所有课程成绩。 73 | 74 | - `python zjuscore.py -ls <学年> <学期>` 可查询某一学期的成绩。您可将 `<学年>` 替换为 `2021` 或 `2021-2022`,并将 `<学期>` 替换为 `春` `夏` `秋` `冬` 以及 `春夏` `秋冬` 等来查询 2021-2022 学年某一学期的所有课程成绩。 75 | 76 | 例如: 77 | 78 | ```powershell 79 | PS > python zjuscore.py -ls 80 | Semeter Name Mark GP Credit 81 | 2021-2022 春夏 离散数学及其应用 60 1.5 4.0 82 | 2021-2022 夏 社会主义发展史 79 3.3 1.5 83 | ...... 84 | 85 | PS > python zjuscore.py -ls 2021 86 | Semeter Name Mark GP Credit 87 | 2021-2022 春夏 离散数学及其应用 60 1.5 4.0 88 | ...... 89 | 90 | PS > python zjuscore.py -ls 2021 夏 91 | Semeter Name Mark GP Credit 92 | 2021-2022 夏 社会主义发展史 79 3.3 1.5 93 | ``` 94 | 95 | 除此之外,您也可以通过 `-n` 或 `--name` 以根据课程名称搜索成绩信息。 96 | 97 | ```powershell 98 | PS > python zjuscore.py -n 离散 99 | Semeter Name Mark GP Credit 100 | 2021-2022 春夏 离散数学及其应用 60 1.5 4.0 101 | 102 | PS > python zjuscore.py -n 微寄分 大物 103 | Semeter Name Mark GP Credit 104 | 2021-2022 春夏 微积分(甲)Ⅱ 80 3.3 5.0 105 | 2021-2022 秋冬 微积分(甲)Ⅰ 80 3.3 5.0 106 | 2021-2022 春夏 大学物理(乙)Ⅰ 80 3.3 3.0 107 | 108 | PS > python zjuscore.py --name 汇编 109 | Cannot find any course matching keyword(s) 汇编 110 | ``` 111 | 112 | ### 均绩计算 113 | 114 | 通过 `-g` 或 `--gpa` 来获取某一特定时间段的均绩。其用法与 `-ls` 参数用法一致。 115 | 116 | ```powershell 117 | PS > python zjuscore.py -g 118 | Your GPA during the whole college is 3.95 119 | 120 | PS > python zjuscore.py -g 2021 121 | Your GPA during the academic year of 2021-2022 is 3.95 122 | 123 | PS > python zjuscore.py -g 2021 夏 124 | Your GPA during the semester of 2021-2022 夏 is 3.90 125 | ``` 126 | 127 | ### 成绩更新通知 128 | 129 | 在启用前,您需要使用 `-d` 或 `--ding` 来设置钉钉群机器人的 Webhook URL: 130 | 131 | - `python zjuscore.py -d https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxxx` 设置钉钉群机器人的 Webhook URL。 132 | 133 | - `python zjuscore.py -d` 重置钉钉群机器人的 Webhook URL。这意味着您可以通过这个命令来关闭钉钉机器人的成绩更新通知。 134 | 135 | 在配置钉钉群机器人时,您可以遵循以下步骤: 136 | 137 | 1. 在钉钉的新手体验群中添加自定义机器人。 138 | 2. 在自定义机器人的安全设置中,设置关键词为 `成绩`。 139 | 3. 复制钉钉机器人提供的 Webhook URL 并通过 `-d` 参数配置到本应用中。 140 | 141 | 之后,使用 `python zjuscore.py -dn` 或 `python zjuscore.py -dnotification` 即可启用成绩更新通知。该应用会持续运行,每隔 1 - 5 分钟自动从教务网同步成绩以获取更新的成绩信息,并将更新成绩通过钉钉群机器人推送至钉钉。 142 | 143 | **注意** 为了更好的使用体验,推荐您使用该功能时将此应用放在服务器上运行。 144 | 145 | 一旦有更新的成绩,您的群机器人会自动推送如下信息: 146 | 147 | ![](./screenshot/notification.jpg) 148 | 149 | ![](./screenshot/dingtalkrobot.jpg) 150 | 151 | ### 参数连用 152 | 153 | 您可以连续使用多个参数来简化使用流程。例如: 154 | 155 | - 使用 `python zjuscore.py -i 321010xxxx xxxxxxxx -u -g` 来初始化成绩助手并获取您的均绩。 156 | - 使用 `python zjuscore.py -i 321010xxxx xxxxxxxx -d https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxxx -dn` 来初始化成绩更新提醒服务并启用通知。 157 | - 使用 `python zjuscore.py -u -n xxx` `python zjuscore.py -u -ls` 或 `python zjuscore.py -u -ls` 使得本应用每次查询成绩信息时都会从教务网重新同步信息。 -------------------------------------------------------------------------------- /scorenotification.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import re 3 | import json 4 | import math 5 | import time 6 | import random 7 | 8 | def rsa_no_padding(src, modulus, exponent): 9 | m = int(modulus, 16) 10 | e = int(exponent, 16) 11 | t = bytes(src, 'ascii') 12 | # 字符串转换为bytes 13 | input_nr = int.from_bytes(t, byteorder='big') 14 | # 将字节转化成int型数字,如果没有标明进制,看做ascii码值 15 | crypt_nr = pow(input_nr, e, m) 16 | # 计算x的y次方,如果z在存在,则再对结果进行取模,其结果等效于pow(x,y) %z 17 | length = math.ceil(m.bit_length() / 8) 18 | # 取模数的比特长度(二进制长度),除以8将比特转为字节 19 | crypt_data = crypt_nr.to_bytes(length, byteorder='big') 20 | # 将密文转换为bytes存储(8字节),返回hex(16字节) 21 | return crypt_data.hex() 22 | 23 | def updatescore(): 24 | session = requests.session() 25 | 26 | # 打开网站 27 | res = session.get('https://zjuam.zju.edu.cn/cas/login?service=https://zdbk.zju.edu.cn/jwglxt/xtgl/login_ssologin.html') 28 | # 获取execution的值以用于登录 29 | execution = re.findall(r'', res.text)[0] 30 | # 获取RSA公钥 31 | res = session.get('https://zjuam.zju.edu.cn/cas/v2/getPubKey') 32 | modulus = res.json()['modulus'] 33 | exponent = res.json()['exponent'] 34 | 35 | with open('database.json', 'r') as f: 36 | userdata = json.load(f) 37 | username = userdata['username'] 38 | password = userdata['password'] 39 | url = userdata.get('url', 'https://oapi.dingtalk.com/robot/send?access_token=') 40 | 41 | rsapwd = rsa_no_padding(password, modulus, exponent) 42 | 43 | data = { 44 | 'username': username, 45 | 'password': rsapwd, 46 | 'execution': execution, 47 | '_eventId': 'submit' 48 | } 49 | # 登录 50 | res = session.post('https://zjuam.zju.edu.cn/cas/login?service=https://zdbk.zju.edu.cn/jwglxt/xtgl/login_ssologin.html', data) 51 | 52 | headers = { 53 | 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; Redmi K30 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Mobile Safari/537.36', 54 | } 55 | 56 | res = session.post( 57 | url=f"https://zdbk.zju.edu.cn/jwglxt/cxdy/xscjcx_cxXscjIndex.html?doType=query&gnmkdm=N5083&su={username}", 58 | data={ 59 | "xn": None, 60 | "xq": None, 61 | "zscjl": None, 62 | "zscjr": None, 63 | "_search": "false", 64 | "nd": str(int(time.time() * 1000)), 65 | "queryModel.showCount": "5000", 66 | "queryModel.currentPage": "1", 67 | "queryModel.sortName": "xkkh", 68 | "queryModel.sortOrder": "asc", 69 | "time": 0, 70 | }, 71 | headers=headers, 72 | ) 73 | 74 | new_score = res.json()['items'] 75 | 76 | try: 77 | with open("dingscore.json", 'r') as load_f: 78 | userscore = json.load(load_f) 79 | except json.decoder.JSONDecodeError: 80 | userscore = {} 81 | except FileNotFoundError: 82 | userscore = {} 83 | 84 | totcredits = 0 85 | totgp = 0 86 | for lesson in userscore: 87 | if userscore[lesson]['score'] in ['合格', '不合格', '弃修']: 88 | continue 89 | if str.isalpha(userscore[lesson]['score'][0]): 90 | # 国际课程用于计算国际化分数的课成绩为字母开头,绩点为0,计入总学分不计入均绩 91 | if float(userscore[lesson]['gp']) == 0.0: 92 | continue 93 | totgp += float(userscore[lesson]['gp']) * float(userscore[lesson]['credit']) 94 | totcredits += float(userscore[lesson]['credit']) 95 | try: 96 | gpa = totgp / totcredits 97 | except: 98 | gpa = 0 99 | 100 | #对比以更新 101 | for lesson in new_score: 102 | id = lesson['xkkh'] 103 | name = lesson['kcmc'] 104 | score = lesson['cj'] 105 | credit = lesson['xf'] 106 | gp = lesson['jd'] 107 | if id == '选课课号': 108 | continue 109 | if userscore.get(id) != None: 110 | continue 111 | 112 | #新的成绩更新 113 | userscore[id] = { 114 | 'name': name, 115 | 'score': score, 116 | 'credit': credit, 117 | 'gp': gp 118 | } 119 | newtotcredits = 0 120 | newtotgp = 0 121 | for lesson in userscore: 122 | if userscore[lesson]['score'] in ['合格', '不合格', '弃修']: 123 | continue 124 | if str.isalpha(userscore[lesson]['score'][0]): 125 | if float(userscore[lesson]['gp']) == 0.0: 126 | continue 127 | newtotgp += float(userscore[lesson]['gp']) * float(userscore[lesson]['credit']) 128 | newtotcredits += float(userscore[lesson]['credit']) 129 | try: 130 | newgpa = newtotgp / newtotcredits 131 | except: 132 | newgpa = 0 133 | 134 | #钉钉推送消息 135 | try: 136 | requests.post(url=url, json={ 137 | "msgtype": "markdown", 138 | "markdown" : { 139 | "title": "考试成绩通知", 140 | "text": """ 141 | ### 考试成绩通知\n 142 | - **选课课号**\t%s\n 143 | - **课程名称**\t%s\n 144 | - **成绩**\t%s\n 145 | - **学分**\t%s\n 146 | - **绩点**\t%s\n 147 | - **成绩变化**\t%.2f(%+.2f) / %.1f(%+.1f)""" % (id, name, score, credit, gp, newgpa, newgpa - gpa, newtotcredits, newtotcredits - totcredits) 148 | } 149 | }) 150 | except requests.exceptions.MissingSchema: 151 | print('The DingTalk Webhook URL is invalid. Please use -d [DingWebhook] to reset it first.') 152 | 153 | print('考试成绩通知\n选课课号\t%s\n课程名称\t%s\n成绩\t%s\n学分\t%s\n绩点\t%s\n成绩变化\t%.2f(%+.2f) / %.1f(%+.1f)' % (id, name, score, credit, gp, newgpa, newgpa - gpa, newtotcredits, newtotcredits - totcredits)) 154 | totcredits = newtotcredits 155 | totgp = newtotgp 156 | gpa = newgpa 157 | 158 | #保存新的数据 159 | with open("dingscore.json", 'w') as load_f: 160 | load_f.write(json.dumps(userscore)) 161 | 162 | def scorenotification(): 163 | while True: 164 | try: 165 | updatescore() 166 | except Exception as e: 167 | print(time.ctime() + " " + str(e)) 168 | finally: 169 | time.sleep(random.randint(60, 300)) 170 | 171 | if __name__ == "__main__": 172 | scorenotification() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZJUScoreAssistant 2 | 3 | Assistant of ZJU score. 4 | 5 | This is a very naive command line score assistant, which is used for querying your score in ZJU, calculating your GPA, score update notification and so on. 6 | 7 | [中文文档](./README_CN.md) 8 | 9 | ## Run 10 | 11 | Download the zip from this repository and unzip, or git this repository to your computer. Then run `python zjuscore.py -h` to get the help. 12 | 13 | ## Usage Manual 14 | 15 | You can use the score assistant through the following arguments: 16 | 17 | ```powershell 18 | python zjuscore.py [-h] [-i username password] [-u] [-ls [YEAR [SEMESTER ...]]] [-n NAME [NAME ...]] 19 | [-g [YEAR [SEMESTER ...]]] [-d [DingWebhook]] [-dn] 20 | ``` 21 | 22 | ### Get Help 23 | 24 | Use `-h` or `--help` to get help. 25 | 26 | ```powershell 27 | PS > python zjuscore.py -h 28 | usage: zjuscore.py [-h] [-i] [-u] [-ls [YEAR [SEMESTER ...]]] [-n NAME [NAME ...]] 29 | [-g [YEAR [SEMESTER ...]]] [-d [DingWebhook]] [-dn] 30 | 31 | ZJU Score Assistant 32 | 33 | options: 34 | -h, --help show this help message and exit 35 | -i, --initial initialize your information 36 | -u, --update update the course score 37 | -ls [YEAR [SEMESTER ...]], --list [YEAR [SEMESTER ...]] 38 | list the course and score in a certain year/semester 39 | -n NAME [NAME ...], --name NAME [NAME ...] 40 | search score by the name of the course 41 | -g [YEAR [SEMESTER ...]], --gpa [YEAR [SEMESTER ...]] 42 | calculator the gpa 43 | -d [DingWebhook], --ding [DingWebhook] 44 | set your DingTalk Robot Webhook. Empty means disabled 45 | -dn, --dnotification enable dingtalk score notification 46 | ``` 47 | 48 | ### Initialize the Score Assistant 49 | 50 | You need to log in to get your score information, so it is necessary to let the assistant to know your username (usually your student ID) and password. You can use `-i` or `--initial` to initialize. Then the program will ask for your information and automatically verify your username and password. Your information will be saved on your computer. 51 | 52 | ```powershell 53 | PS > python zjuscore.py -i 54 | ZJUAM account's username: 3200106666 55 | ZJUAM 3200106666's password: 56 | Error: Invalid username or password. Please check them again and use -i to reset them. 57 | 58 | PS > python zjuscore.py -i 59 | ZJUAM account's username: 3200106666 60 | ZJUAM 3200106666's password: 61 | Done: Initial Success! 62 | ``` 63 | 64 | ### Update the Score on your Computer 65 | 66 | You can use the argument `-u` or `--update` to update the score information stord on your computer. This operation is used to avoid wasting time by having to get your information every time you use the program. 67 | 68 | ```powershell 69 | PS > python zjuscore.py -u 70 | Updated Success! 71 | ``` 72 | 73 | ### Score Query 74 | 75 | Use `-ls` or `--list` to query your score. It supports the following three ways to use it: 76 | 77 | - `python zjuscore.py -ls` can query all your score information during college. 78 | - `python zjuscore.py -ls ` can query your information during a certain academic year. You can replace `` with `2021` or `2021-2022` to query all your courses' score in the 2021-2022 academic year. 79 | - `python zjuscore.py -ls ` can query your score information during a certain semester. You can replace `` with `2021` or `2021-2022`, and replace `` with `春` `夏` `秋` `冬` or `春夏` `秋冬` and so on, to query all the grades of courses in a certain semester of 2021-2022 academic year. 80 | 81 | For example: 82 | ```powershell 83 | PS > python zjuscore.py -ls 84 | Semeter Name Mark GP Credit 85 | 2021-2022 春夏 离散数学及其应用 60 1.5 4.0 86 | 2021-2022 夏 社会主义发展史 79 3.3 1.5 87 | ...... 88 | 89 | PS > python zjuscore.py -ls 2021 90 | Semeter Name Mark GP Credit 91 | 2021-2022 春夏 离散数学及其应用 60 1.5 4.0 92 | ...... 93 | 94 | PS > python zjuscore.py -ls 2021 夏 95 | Semeter Name Mark GP Credit 96 | 2021-2022 夏 社会主义发展史 79 3.3 1.5 97 | ``` 98 | 99 | In addition, you can use `-n` or `--name` to search for score information, the name of which matching the course name in the following argument(s). 100 | 101 | ```powershell 102 | PS > python zjuscore.py -n 离散 103 | Semeter Name Mark GP Credit 104 | 2021-2022 春夏 离散数学及其应用 60 1.5 4.0 105 | 106 | PS > python zjuscore.py -n 微寄分 大物 107 | Semeter Name Mark GP Credit 108 | 2021-2022 春夏 微积分(甲)Ⅱ 80 3.3 5.0 109 | 2021-2022 秋冬 微积分(甲)Ⅰ 80 3.3 5.0 110 | 2021-2022 春夏 大学物理(乙)Ⅰ 80 3.3 3.0 111 | 112 | PS > python zjuscore.py --name 汇编 113 | Cannot find any course matching keyword(s) 汇编 114 | ``` 115 | 116 | ### Calculate GPA 117 | 118 | Use `-g` or `--gpa` to obtain your GPA of a certain period. The argument(s) and usage of this is consistent with those of `-ls`. 119 | 120 | ```powershell 121 | PS > python zjuscore.py -g 122 | Your GPA during the whole college is 3.95 123 | 124 | PS > python zjuscore.py -g 2021 125 | Your GPA during the academic year of 2021-2022 is 3.95 126 | 127 | PS > python zjuscore.py -g 2021 夏 128 | Your GPA during the semester of 2021-2022 夏 is 3.90 129 | ``` 130 | 131 | ### Score Update Notification 132 | 133 | Before running the notification assistant, you should use `-d` or `--ding` to set the URL of DingTalk Robot. 134 | 135 | - `python zjuscore.py -d https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxxx` Set the URL of dingtalk robot webhook. 136 | - `python zjuscore.py -d` Reset the URL of dingtalk robot webhook. This means that you can unable the notification assistant in dingtalk. 137 | 138 | You should configure your Dingtalk Robot first to get the URL. You can follow the following steps: 139 | 140 | 1. Add custom robot. 141 | 2. In the robot security setting, just add `成绩` to the custom keyword. You can customized your robot's photo and name like the following examples. 142 | 3. Copy the webhook URL provided by the robot and use `-d` to tell the notificaiton assistant mentioned above. 143 | 144 | After that, use `python zjuscore.py -dn` or `python zjuscore.py -dnotification` to enable the score update notification. The application will run continuously and synchronize your score from ZJU every 1 to 5 minutes and inform you the updated information by DingTalk Robot. 145 | 146 | Once there is an updated information, your dingtalk robot will push the following information automatically: 147 | 148 | ![](./screenshot/notification.jpg) 149 | 150 | ![](./screenshot/dingtalkrobot.jpg) 151 | 152 | **NOTICE** For a better experience, it is recommended that you put this application on the server when you use notification assistant. 153 | 154 | ### Arguments Combination 155 | 156 | Use the mutiple combination of arguments to simplied the use process. For example: 157 | 158 | - Run `python zjuscore.py -i -u -g` to initialize and obtain your GPA. 159 | - Run `python zjuscore.py -i -d https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxxx -dn` to initialize and enable the notification assistant. 160 | - Run `python zjuscore.py -u -n xxx` `python zjuscore.py -u -ls` or `python zjuscore.py -u -ls` to make the assistant resynchronize your score information from ZJU every time you query the score information. -------------------------------------------------------------------------------- /zjuscore.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import getpass 3 | import json 4 | import difflib 5 | import sys 6 | import requests 7 | import colorama 8 | from colorama import Fore 9 | from zjusess import zjusess 10 | from scorenotification import scorenotification 11 | 12 | # 用于中文对齐输出 13 | def pad_len(string, length): 14 | return length - len(string.encode('GBK')) + len(string) 15 | 16 | class LOG: 17 | info = Fore.CYAN + 'Info: ' + Fore.RESET 18 | warning = Fore.YELLOW + 'Warning: ' + Fore.RESET 19 | error = Fore.RED + 'Error: ' + Fore.RESET 20 | done = Fore.GREEN + 'Done: ' + Fore.RESET 21 | tips = Fore.MAGENTA + 'Tips: ' + Fore.RESET 22 | default = '' 23 | 24 | def print_log(log : LOG, *args, **kwargs): 25 | print(log, end='') 26 | print(*args, **kwargs) 27 | 28 | if __name__ == '__main__': 29 | parser = argparse.ArgumentParser(description='ZJU Score Assistant') 30 | parser.add_argument('-i', '--initial', action='store_true', help='initialize your information') 31 | parser.add_argument('-u', '--update', action='store_true', help='update the course score') 32 | parser.add_argument('-ls', '--list', nargs='*', metavar=('YEAR', 'SEMESTER'), help='list the course and score in a certain year/semester') 33 | parser.add_argument('-n', '--name', nargs='+', help='search score by the name of the course') 34 | parser.add_argument('-g', '--gpa', nargs='*', metavar=('YEAR', 'SEMESTER'), help='calculator the gpa') 35 | parser.add_argument('-d', '--ding', nargs='?', metavar=('DingWebhook'), default=argparse.SUPPRESS, help='set your DingTalk Robot Webhook. Empty means disabled') 36 | parser.add_argument('-dn', '--dnotification', action='store_true', help='enable dingtalk score notification') 37 | args = parser.parse_args() 38 | 39 | colorama.init(autoreset=True) 40 | 41 | if args.initial: 42 | 43 | username = input("ZJUAM account's username: ") 44 | password = getpass.getpass(f"ZJUAM {username}'s password: ") 45 | 46 | database = { 47 | 'username': username, 48 | 'password': password, 49 | } 50 | 51 | session = zjusess() 52 | try: 53 | if not session.login(username, password): 54 | print_log(LOG.error, 'Invalid username or password. Please check them again and use -i to reset them.') 55 | sys.exit() 56 | except requests.exceptions.ConnectionError: 57 | print_log(LOG.error, 'Cannot connect to the Internet. Please check your Internet connection.') 58 | else: 59 | with open("database.json", 'w') as load_f: 60 | load_f.write(json.dumps(database)) 61 | print_log(LOG.done, 'Initial Success!') 62 | session.close() 63 | 64 | data = {} 65 | if args.update: 66 | session = zjusess() 67 | try: 68 | with open('database.json', 'r') as f: 69 | userdata = json.load(f) 70 | except: 71 | print_log(LOG.error, 'Cannot find your user data. Please use -i to initialize.') 72 | sys.exit() 73 | username = userdata['username'] 74 | password = userdata['password'] 75 | try: 76 | res = session.login(username, password) 77 | except requests.exceptions.ConnectionError: 78 | print_log(LOG.error, 'Cannot connect to the Internet. Please check your Internet connection.') 79 | else: 80 | if not res: 81 | print_log(LOG.error, 'Login failed. Please check your username and password. Remember to use -i to reset them.') 82 | else: 83 | try: 84 | #打开成绩查询网站 85 | res = session.get(r'http://appservice.zju.edu.cn/zdjw/cjcx/cjcxjg') 86 | res = session.post('http://appservice.zju.edu.cn/zju-smartcampus/zdydjw/api/kkqk_cxXscjxx') 87 | 88 | data = dict(enumerate(res.json()['data']['list'])) 89 | with open('userscore.json', 'w') as f: 90 | f.write(json.dumps(data)) 91 | print_log(LOG.done, 'Updated Successfully!') 92 | except: 93 | print_log(LOG.error, 'Cannot connect to the Internet. Please check your Internet connection.') 94 | session.close() 95 | else: 96 | try: 97 | with open('userscore.json', 'r') as f: 98 | data = json.load(f) 99 | except: 100 | print_log(LOG.error, 'Cannot find your score data, please use -u to update first.') 101 | 102 | if args.list != None: 103 | if len(args.list) == 0: 104 | 105 | courses = data.values() 106 | if len(courses) == 0: 107 | print_log(LOG.info, f'Cannot find any courses during the whole college.') 108 | print_log(LOG.tips, 'Maybe you need to use -u to update first :)') 109 | else: 110 | print(f'{"Semeter":16s}{"Name":20s}\tMark\tGP\tCredit') 111 | for course in courses: 112 | print('{0:<{len0}}{1:<{len1}}\t{2}\t{3}\t{4}'.format( 113 | f"{course.get('xn')} {course.get('xq')}", 114 | course.get('kcmc'), 115 | course.get('cj'), 116 | course.get('jd'), 117 | course.get('xf'), 118 | len0 = pad_len(f"{course.get('xn')} {course.get('xq')}", 16), 119 | len1 = pad_len(course.get('kcmc'), 20))) 120 | 121 | elif len(args.list) == 1: 122 | 123 | courses = [i for i in data.values() if i.get('xn').find(args.list[0]) == 0] 124 | 125 | if len(courses) == 0: 126 | print_log(LOG.info, f'Cannot find any courses about the academic year of {args.list[0]}.') 127 | print_log(LOG.tips, 'Maybe you need to use -u to update first :)') 128 | else: 129 | print(f'{"Semeter":16s}{"Name":20s}\tMark\tGP\tCredit') 130 | for course in courses: 131 | print('{0:<{len0}}{1:<{len1}}\t{2}\t{3}\t{4}'.format( 132 | f"{course.get('xn')} {course.get('xq')}", 133 | course.get('kcmc'), 134 | course.get('cj'), 135 | course.get('jd'), 136 | course.get('xf'), 137 | len0 = pad_len(f"{course.get('xn')} {course.get('xq')}", 16), 138 | len1 = pad_len(course.get('kcmc'), 20))) 139 | 140 | elif len(args.list) >= 2: 141 | if len(args.list) > 2: 142 | print_log(LOG.warning, f'The following argument(s) will be ignored:\n\t{" ".join(args.list[2:])}') 143 | 144 | courses = [i for i in data.values() if i.get('xn').find(args.list[0]) == 0 and args.list[1].find(i.get('xq', '-1')) != -1] 145 | 146 | if len(courses) == 0: 147 | print_log(LOG.info, f'Cannot find any courses about the semester of {" ".join(args.list[:2])}') 148 | print_log(LOG.tips, 'Maybe you need to use -u to update first :)') 149 | else: 150 | print(f'{"Semeter":16s}{"Name":20s}\tMark\tGP\tCredit') 151 | for course in courses: 152 | print('{0:<{len0}}{1:<{len1}}\t{2}\t{3}\t{4}'.format( 153 | f"{course.get('xn')} {course.get('xq')}", 154 | course.get('kcmc'), 155 | course.get('cj'), 156 | course.get('jd'), 157 | course.get('xf'), 158 | len0 = pad_len(f"{course.get('xn')} {course.get('xq')}", 16), 159 | len1 = pad_len(course.get('kcmc'), 20))) 160 | 161 | if args.name: 162 | coursename = [i.get('kcmc') for i in data.values()] 163 | res = [] 164 | for searchcourse in args.name: 165 | res += difflib.get_close_matches(searchcourse, coursename, cutoff=0.3) 166 | res = list(dict().fromkeys(res).keys()) 167 | if len(res) == 0: 168 | print_log(LOG.info, f'Cannot find any course matching keyword(s) {" ".join(args.name)}') 169 | else: 170 | print(f'{"Semeter":16s}{"Name":20s}\tMark\tGP\tCredit') 171 | for name in res: 172 | for course in data.values(): 173 | if course.get('kcmc') == name: 174 | print('{0:<{len0}}{1:<{len1}}\t{2}\t{3}\t{4}'.format( 175 | f"{course.get('xn')} {course.get('xq')}", 176 | course.get('kcmc'), 177 | course.get('cj'), 178 | course.get('jd'), 179 | course.get('xf'), 180 | len0 = pad_len(f"{course.get('xn')} {course.get('xq')}", 16), 181 | len1 = pad_len(course.get('kcmc'), 20))) 182 | 183 | if args.gpa != None: 184 | if len(args.gpa) == 0: 185 | 186 | grade = [i.get('jd') for i in data.values() if i.get('cj') not in ['合格', '不合格', '弃修']] 187 | credit = [float(i.get('xf')) for i in data.values() if i.get('cj') not in ['合格', '不合格', '弃修']] 188 | 189 | if len(grade) == 0: 190 | print_log(LOG.info, f'Cannot find any courses during the whole college.') 191 | print_log(LOG.tips, 'Maybe you need to use -u to update first :)') 192 | else: 193 | gp = .0 194 | for i in range(len(grade)): 195 | gp += grade[i] * credit[i] 196 | totcredit = sum(credit) 197 | gpa = 0 198 | if totcredit != 0: 199 | gpa = gp / totcredit 200 | print_log(LOG.done, 'Your GPA during the whole college is %.2f and GP is %.2f' % (gpa, gp)) 201 | 202 | elif len(args.gpa) == 1: 203 | 204 | grade = [i.get('jd') for i in data.values() if i.get('xn').find(args.gpa[0]) == 0 and i.get('cj') not in ['合格', '不合格', '弃修']] 205 | credit = [float(i.get('xf')) for i in data.values() if i.get('xn').find(args.gpa[0]) == 0 and i.get('cj') not in ['合格', '不合格', '弃修']] 206 | 207 | if len(grade) == 0: 208 | print_log(LOG.info, f'Cannot find any courses about the academic year of {args.gpa[0]}') 209 | print_log(LOG.tips, 'Maybe you need to use -u to update first :)') 210 | else: 211 | gp = .0 212 | for i in range(len(grade)): 213 | gp += grade[i] * credit[i] 214 | totcredit = sum(credit) 215 | gpa = .0 216 | if totcredit != 0: 217 | gpa = gp / totcredit 218 | 219 | year = args.gpa[0] 220 | 221 | for i in data.values(): 222 | if i.get('xn').find(args.gpa[0]) == 0: 223 | year = i.get('xn') 224 | break 225 | 226 | print_log(LOG.done, 'Your GPA during the academic year of %s is %.2f and GP is %.2f' % (year, gpa, gp)) 227 | 228 | elif len(args.gpa) >= 2: 229 | if len(args.gpa) > 2: 230 | print_log(LOG.warning, f'The following argument(s) will be ignored:\n\t{" ".join(args.gpa[2:])}') 231 | 232 | grade = [i.get('jd') for i in data.values() if i.get('xn').find(args.gpa[0]) == 0 and args.gpa[1].find(i.get('xq', '-1')) != -1 and i.get('cj') not in ['合格', '不合格', '弃修']] 233 | credit = [float(i.get('xf')) for i in data.values() if i.get('xn').find(args.gpa[0]) == 0 and args.gpa[1].find(i.get('xq', '-1')) != -1 and i.get('cj') not in ['合格', '不合格', '弃修']] 234 | 235 | if len(grade) == 0: 236 | print_log(LOG.info, f'Cannot find any courses about the semester of {" ".join(args.gpa[:2])}') 237 | print_log(LOG.tips, 'Maybe you need to use -u to update first :)') 238 | else: 239 | gp = .0 240 | for i in range(len(grade)): 241 | gp += grade[i] * credit[i] 242 | totcredit = sum(credit) 243 | gpa = .0 244 | if totcredit != 0: 245 | gpa = gp / totcredit 246 | 247 | year = args.gpa[0] 248 | semster = args.gpa[1] 249 | 250 | for i in data.values(): 251 | if i.get('xn').find(args.gpa[0]) == 0: 252 | year = i.get('xn') 253 | break 254 | 255 | print_log(LOG.done, 'Your GPA during the semester of %s %s is %.2f and GP is %.2f' % (year, semster, gpa, gp)) 256 | 257 | try: 258 | if args.ding: 259 | try: 260 | with open('database.json', 'r') as f: 261 | userdata = json.load(f) 262 | except json.decoder.JSONDecodeError: 263 | userdata = {} 264 | userdata['url'] = args.ding 265 | with open("database.json", 'w') as load_f: 266 | load_f.write(json.dumps(userdata)) 267 | else: 268 | try: 269 | with open('database.json', 'r') as f: 270 | userdata = json.load(f) 271 | except json.decoder.JSONDecodeError: 272 | userdata = {} 273 | userdata['url'] = 'https://oapi.dingtalk.com/robot/send?access_token=' 274 | with open("database.json", 'w') as load_f: 275 | load_f.write(json.dumps(userdata)) 276 | except AttributeError: 277 | pass 278 | 279 | if args.dnotification: 280 | scorenotification() --------------------------------------------------------------------------------