├── README.md ├── img └── 1.png ├── 选课 └── selectCourse.py └── 选课(解决第三轮抢课的加密问题) └── electCourse.py /README.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 本程序是同济大学抢课程序,准确来说是捡漏,目前在第四轮抢体育课(捡漏)使用成功,使用时间为2023-2-20;主要使用爬虫相关的技术,可以定义为半自动脚本 3 | 2024.1.04第三轮抢课中校方对elect请求头进行了加密,在原基础上加入了新版本,在第三轮体育04实验后可用。 4 | ## 注意事项 5 | 1. 本代码只适用于同一课程名称的多个课程序号捡漏(比如体育(4)下有众多种类,可根据喜好选择足球,羽毛球等多个,但暂不支持同时对体育(4)和玉石两个课程名称捡漏),若需扩展程序功能实现多课程名称捡漏需要修改接口信息(也可以考虑将该类多线程) 6 | 2. 避免与已有课程冲突,否则会出现意外错误(程序没有考虑该错误处理) 7 | 3. 当选上某一课时程序结束。比如体育(4)同时选择A老师的足球和B老师的羽毛球,在选上A老师足球课后程序结束 8 | 4. 第三轮选课不一定适用(未测试) 9 | 5. 选课需要先进入选课网页,获取必要信息(所以是半自动脚本)。但如果每学期课程没有变化,可以尝试在下次抢课 10 | 6. 选体育(4)不需要修改接口,其他课程需要修改接口 11 | 7. 在python3.8已成功使用 12 | 13 | ## 使用方式 14 | 1. 参数填写 15 | student_id,cookies中的sessionid,`target_teachClassCode`,分别代表学号,sessionid和欲选课程的代号(对应选课界面中的课程序号),需要自行修改。sessionid获取:打开浏览器开发者模式,调至`network`,进入选课界面,找到`name`为`loginCheck`的接口(可以用Fetch/XHR筛选),在`Headers`中下拉找到`cookie`一行,含有sessionid,复制下来即可,如图![1](./img/1.png) 16 | 如果是选择体育(4)不需要修改接口信息,选择其他课程需要修改`getTeachClass4Limit`变量,具体接口获取与sessionid获取类似,开发者模式下打开对应的课程名称,在开发者面板的`network`中找到新出现的接口,点开找到`RequestURL`一栏,这就是接口信息 17 | 2. 开始运行 18 | 程序路径下执行`python selectCourse`即可(该指令为参考,根据环境变量的不同可能会有不同python),只要能运行起selectCourse即可 19 | 3. 输出信息 20 | 每一轮遍历`target_teachClassCode`逐一提交课程申请,每一轮会有提示当前轮数,每次提交会给出提交课程的信息及结果,当选中课程后,给出提示,结束程序。 21 | 22 | ## 如果觉得还行可以star 23 | 24 | ## 2024-1-4补充 25 | 据反馈,本次选课中calendarId,round_id,electResUrl等有改变,使用请注意需要自行改动 26 | -------------------------------------------------------------------------------- /img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanMoLiuHen/selectCourse/51f7be9a45163258630a9c05bcc06bdc69f6419d/img/1.png -------------------------------------------------------------------------------- /选课/selectCourse.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | import requests 4 | # 学号,需要修改 5 | student_id='' 6 | # 浏览器内查看的sessionid,需要修改(填充)14c 7 | cookies={"sessionid":""} 8 | # 需要捡漏的课程,需要修改(填充) 9 | target_teachClassCode={''} 10 | 11 | 12 | # 一般不用修改 13 | round_id='5276' 14 | electUrl = 'https://1.tongji.edu.cn/api/electionservice/student/elect' 15 | electResUrl = 'https://1.tongji.edu.cn/api/electionservice/student/5276/electRes' 16 | 17 | # 体育课选择接口,需要修改,从浏览器开发者模式获取接口 18 | getTeachClass4Limit='https://1.tongji.edu.cn/api/electionservice/student/getTeachClass4Limit?roundId=5276&courseCode=320004&studentId='+student_id+'&calendarId=115' 19 | 20 | class SelectClass(): 21 | def __init__(self): 22 | self.student_id=student_id 23 | self.cookies=cookies 24 | self.target_teachClassCode=target_teachClassCode 25 | self.all_PEclass={} 26 | 27 | def getAllPE(self): 28 | # 请求所有的体育课 29 | res=requests.post(getTeachClass4Limit,cookies=cookies) 30 | if res.status_code==200: 31 | self.all_PEclass= json.loads(res.text)['data'] 32 | else: 33 | print('请求失败,请检查sessionid等信息,程序已退出') 34 | exit() 35 | 36 | # 请求一个课程 37 | def chooseAClass(self,data,class_information): 38 | print('[INFO]开始请求 '+class_information['value']+' 课程') 39 | # 发送请求 40 | res=requests.post(electUrl,cookies=cookies,json=data) 41 | if res.status_code==200: 42 | print('[INFO]send success') 43 | else: 44 | print(res.text) 45 | time.sleep(3) 46 | # 接收响应 47 | res = requests.post(electResUrl, cookies=cookies) 48 | resJson = json.loads(res.text) 49 | if res.status_code==200: 50 | # 如果接收响应成功查看结果,是否选上 51 | if resJson['data']['failedReasons']=={}: 52 | print('成功 Congratulation! 你选上了') 53 | exit('选课结束,退出') 54 | else: 55 | try: 56 | print('[INFO]失败',resJson['data']['failedReasons']) 57 | except: 58 | print(resJson) 59 | 60 | # 开始请求 61 | def requestPE(self): 62 | # 根据课程序号生成对应的请求体 63 | for one in self.all_PEclass: 64 | if one['teachClassCode'] in target_teachClassCode: 65 | paylaod={'roundId':5276,'elecClassList':[{"teachClassId":one['teachClassId'],"teachClassCode":one['teachClassCode'],"courseCode":one['courseCode'],"courseName":one['courseName'],"teacherName":one['teacherName']}],"withdrawClassList":[]} 66 | self.chooseAClass(paylaod,{'value':one['times'][0]['value']}) 67 | print() 68 | 69 | if __name__=='__main__': 70 | a=SelectClass() 71 | a.getAllPE() 72 | i=1 73 | while(1): 74 | time.sleep(1) 75 | print('第'+str(i)+'轮申请课程') 76 | a.requestPE() 77 | i+=1 78 | 79 | -------------------------------------------------------------------------------- /选课(解决第三轮抢课的加密问题)/electCourse.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver import Edge 2 | from selenium.webdriver.common.by import By 3 | from selenium import webdriver 4 | import time 5 | import requests 6 | import json 7 | import base64 8 | import hashlib 9 | import urllib.parse 10 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes 11 | from cryptography.hazmat.backends import default_backend 12 | from cryptography.hazmat.primitives import padding 13 | 14 | # 获取 sessionid 的函数 15 | def get_sessionid(): 16 | driver = Edge() 17 | 18 | # 打开登录页面 19 | login_url = "https://1.tongji.edu.cn" # 登录 URL 20 | driver.get(login_url) 21 | 22 | # 等待页面加载 23 | time.sleep(5) 24 | 25 | # 定位用户名和密码输入框,并输入登录信息 26 | username = driver.find_element(By.ID, "j_username") 27 | password = driver.find_element(By.ID, "j_password") 28 | # 一系统自己的账密 29 | username.send_keys("") 30 | password.send_keys("") 31 | 32 | # 定位登录按钮并点击 33 | login_button = driver.find_element(By.ID, "loginButton") 34 | login_button.click() 35 | 36 | # 等待登录过程 37 | time.sleep(3) 38 | 39 | # 提取 sessionid 40 | cookies = driver.get_cookies() 41 | sessionid = next((cookie['value'] for cookie in cookies if cookie['name'] == 'sessionid'), None) 42 | 43 | # 关闭浏览器 44 | driver.quit() 45 | 46 | return sessionid 47 | 48 | def get_course_elect_secret(): 49 | url = "https://www.gardilily.com/oneDotTongji/courseElectSecret.php" 50 | 51 | try: 52 | response = requests.get(url) 53 | if response.status_code != 200: 54 | return None 55 | 56 | json_data = response.json() 57 | return { 58 | 'key': json_data['key'], 59 | 'iv': json_data['iv'] 60 | } 61 | 62 | except Exception as e: 63 | print(f"发生错误: {e}") 64 | return None 65 | # AES Encryption 66 | def encrypt_aes_cbc(input_data, key, iv): 67 | backend = default_backend() 68 | cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend) 69 | encryptor = cipher.encryptor() 70 | padder = padding.PKCS7(128).padder() 71 | 72 | padded_data = padder.update(input_data.encode()) + padder.finalize() 73 | return encryptor.update(padded_data) + encryptor.finalize() 74 | 75 | def encrypt_data(student_id, course_code, teach_class_id, calendar_id, timestamp, payload_json): 76 | secret = get_course_elect_secret() 77 | if secret: 78 | secret_key = secret['key'].encode() 79 | iv = secret['iv'].encode() 80 | # Concatenating elements 81 | elements_plus = f"{student_id}+{course_code}+{teach_class_id}+{calendar_id}+{timestamp}+{payload_json}" 82 | elements_and = f"{student_id}&{course_code}&{teach_class_id}&{calendar_id}&{timestamp}&{payload_json}" 83 | # MD5 Hash 84 | md5_hash = hashlib.md5(elements_plus.encode()).hexdigest() 85 | # Encrypt and encode 86 | encrypted_data = encrypt_aes_cbc(urllib.parse.quote(elements_and), secret_key, iv) 87 | encoded_data = base64.b64encode(encrypted_data).decode() 88 | # URL encode the base64 encoded encrypted data 89 | cipher_url_encoded = urllib.parse.quote(encoded_data) 90 | # Final JSON Object 91 | encrypted_json_str = { 92 | "checkCode": md5_hash, 93 | "ciphertext": cipher_url_encoded 94 | } 95 | return encrypted_json_str # 这是加密后的 JSON 字符串 96 | 97 | 98 | class SelectClass(): 99 | def __init__(self): 100 | self.student_id = ''# 填自己的学号 101 | self.cookies = {"sessionid": get_sessionid()} 102 | self.target_teachClassCode = {'32000428', '32000473', '32000411', '32000432'}# 一个例子,根据需要更改 103 | self.all_PEclass = {} 104 | self.round_id = '5343'# 需要按自己的更改 105 | self.electUrl = 'https://1.tongji.edu.cn/api/electionservice/student/elect' 106 | self.electResUrl = f'https://1.tongji.edu.cn/api/electionservice/student/{self.round_id}/electRes' 107 | # 需要按自己的更改 108 | self.getTeachClass4Limit = f'https://1.tongji.edu.cn/api/electionservice/student/getTeachClass4Limit?roundId={self.round_id}&courseCode=320004&studentId={self.student_id}&calendarId=116' 109 | 110 | def getAllPE(self): 111 | # 请求所有的体育课 112 | res=requests.post(self.getTeachClass4Limit,cookies=self.cookies) 113 | if res.status_code==200: 114 | self.all_PEclass= json.loads(res.text)['data'] 115 | else: 116 | print('请求失败,请检查sessionid等信息,程序已退出') 117 | exit() 118 | 119 | # 请求一个课程 120 | def chooseAClass(self,data,class_information): 121 | print('[INFO]开始请求 '+class_information['value']+' 课程') 122 | # 在这里加入加密逻辑 123 | encrypted_data = encrypt_data( 124 | self.student_id, 125 | data['elecClassList'][0]['courseCode'], 126 | data['elecClassList'][0]['teachClassId'], 127 | '116', # 假设 calendar_id 是 116 128 | str(int(time.time() * 1000)), # 当前时间戳 129 | json.dumps(data) # 转换为 JSON 字符串 130 | ) 131 | # 发送加密的数据 132 | res = requests.post(self.electUrl, cookies=self.cookies, data=encrypted_data) 133 | if res.status_code==200: 134 | print('[INFO]send success') 135 | else: 136 | print(res.text) 137 | 138 | # 循环等待直到状态变为 Ready 139 | while True: 140 | time.sleep(2) # 等待一段时间再次检查状态 141 | res = requests.post(self.electResUrl, cookies=self.cookies) 142 | if res.status_code != 200: 143 | print('[ERROR]请求失败: 状态码:', res.status_code) 144 | print('[ERROR]响应内容:', res.text) 145 | return 146 | 147 | try: 148 | resJson = json.loads(res.text) 149 | except json.JSONDecodeError as e: 150 | print('[ERROR]解析 JSON 失败:', e) 151 | print('[ERROR]响应内容:', res.text) 152 | return 153 | 154 | # 检查状态 155 | if resJson['data']['status'] == 'Ready': 156 | # 检查选课结果 157 | if resJson['data']['failedReasons'] == {}: 158 | print('成功 Congratulation! 你选上了') 159 | exit('选课结束,退出') 160 | else: 161 | print('[INFO]失败', resJson['data']['failedReasons']) 162 | break 163 | elif resJson['data']['status'] == 'Processing': 164 | print('[INFO]选课正在处理中') 165 | else: 166 | print('[INFO]未知状态:', resJson['data']['status']) 167 | break 168 | 169 | # 开始请求 170 | def requestPE(self): 171 | # 根据课程序号生成对应的请求体 172 | for one in self.all_PEclass: 173 | if one['teachClassCode'] in self.target_teachClassCode: 174 | payload={'roundId':5343,'elecClassList':[{"teachClassId":one['teachClassId'],"teachClassCode":one 175 | ['teachClassCode'],"courseCode":one['courseCode'],"courseName":one['courseName'],"teacherName":one 176 | ['teacherName']}],"withdrawClassList":[]} 177 | self.chooseAClass(payload,{'value':one['times'][0]['value']}) 178 | print() 179 | 180 | def refresh_session(self): 181 | # 每25轮刷新一次 sessionid 182 | self.cookies = {"sessionid": get_sessionid()} 183 | 184 | if __name__=='__main__': 185 | a=SelectClass() 186 | a.getAllPE() 187 | i=1 188 | while True: 189 | if i % 25 == 0: 190 | a.refresh_session() 191 | time.sleep(1) 192 | print(f'第{i}轮申请课程') 193 | a.requestPE() 194 | i += 1 195 | 196 | --------------------------------------------------------------------------------