├── README.md ├── build_script.py ├── requirements.txt ├── resource ├── 1.png ├── 2.png ├── 4.png ├── res1.png ├── res2.jpg ├── res3.jpg └── rysh.jpg └── xk_spider ├── AutoLogin.py ├── GetCourse.py ├── __init__.py ├── api.py └── run.py /README.md: -------------------------------------------------------------------------------- 1 | # YNU-xk_spider 2 | 云南大学选课爬虫,提供余课提醒服务,实现了自动抢课 3 | 4 | [重构版](https://github.com/davidwushi1145/YNU-xk_spider_Refactoring),若存在bug请到此版本提出issue 5 | 6 | * 来源于https://github.com/starwingChen/YNU-xk_spider 7 | * 更进一步解决自动注销问题 2023-6-23测试三小时无注销 8 | * 请自行搭建验证码识别api或自行寻找 9 | * 解决api接口问题 2023-12-28多系统测试无异常 10 | * 2023-12-30 经测试24小时无异常 11 | * 2024-3-8 修复已知的所有bug,若仍然遇到问题请提交issue 12 | * 2024-6-26 修复完成 13 | * 2024-12-25 修复体育课问题及东陆校区问题(注意!!!东陆校区需要修改[GetCourse.py](xk_spider/GetCourse.py)表单中的campus为01) 14 | ## 项目环境: 15 | * python版本:3.10 16 | * 第三方库:selenium 4.1.0;requests ; flask 3.0.0; ddddocr 1.4.10; fake_useragent; 17 | * Chrome版本:最新版本 及其对应driver 18 | 19 | 已经实现了余课提醒和自动抢课,余课提醒是通过server酱接口直接发送到你的微信上,为此你需要先从他们官网上获得一个key([点击访问server酱官网,获取到key即可](https://sct.ftqq.com/)),并且**关注"方糖"服务号**。具体操作官网都有写,我就不赘述了。 20 | 21 | 另外程序主要提供主修(包括必修和专选)、素选课程及体育课的提醒和抢课,**跨专业选修没测试过**,如果遇到问题可以在issue里提出来 22 | 23 | 24 | ## 如何使用: 25 | 1. **安装好运行环境,下载此程序并解压。** 26 | 2. **切换到YNU-xk_spider-master目录** 27 | 3. **运行```pip install -r requirements.txt```** 28 | 4. **运行api.py文件**(!!!!本地识别一定要先运行这个) 29 | 5. **打开run.py文件。** 30 | 6. **按照文件注释中的提示填写好字段,运行程序。** 31 | 需要填的字段都已经用注释的形式标明了,填完直接运行即可。这之后程序会开始循环执行,同时打开一个窗口,登录进去等窗口自己关闭后就可以不用管了 32 | 33 | 我已经尽量把代码封装成小白能使用的程度了,不需要有太多前端和python基础,安装完运行环境,照着注释将字段填好就完事了。程序已经做了初步的异常检测,如果您在运行时有什么问题,也可以在issue里提出来 34 | 35 | 另外,因为程序使用到了selenium模块,因此必须要下载Chrome浏览器驱动。具体教程[参考教程见此,另外不需要添加环境变量,记住你的下载路径就行](https://blog.csdn.net/mingfeng4923/article/details/130989513),如果您的电脑未安装Chrome浏览器,这边建议您安装一个,而且没有Chrome此程序无法运行。chreomeDriver下载地址:https://googlechromelabs.github.io/chrome-for-testing/ 36 | 37 | ## 自行搭建api方法 38 | 39 | 打开api.py 40 | 41 | ```python 42 | pip install ddddocr 43 | pip install flask 44 | ``` 45 | 46 | 然后直接运行api.py 47 | 48 | (也可在腾讯云函数搭建) 49 | 50 | ## 打包好的api.exe 51 | https://drive.google.com/file/d/1IsszQXBuvdYbpmnyibLAGw92p8SdW54T/view?usp=sharing 52 | 在windows x86环境下打包。直接运行后调用http://127.0.0.1:5000/base64img即可 53 | 54 | **如果本项目有帮到你,可以点击右上角的star支持一下 :)** 55 | 56 | ### 云函数搭建方法 57 | 58 | ```shell 59 | docker pull ccr.ccs.tencentyun.com/ocrr/ocr:2.0.0 60 | ``` 61 | 62 | 打完tag后上传到你自己的仓库然后使用云函数docker部署 63 | 64 | ![image-20240103114026538](https://raw.githubusercontent.com/davidwushi1145/photo2/main/image-20240103114026538.png) 65 | 66 | 高级配置拉满 67 | 68 | ![image-20240103114116110](https://raw.githubusercontent.com/davidwushi1145/photo2/main/image-20240103114116110.png) 69 | 70 | 然后测试即可 71 | 72 | ![image-20240103114217426](https://raw.githubusercontent.com/davidwushi1145/photo2/main/image-20240103114217426.png) 73 | 74 | ### 注意 75 | 76 | 使用云函数需要修改AutoLogin.py中的imgcode_online函数 77 | 78 | ```python 79 | def imgcode_online(imgurl): 80 | if not hasattr(imgcode_online, "counter"): 81 | imgcode_online.counter = 0 82 | if not hasattr(imgcode_online, "timestamp"): 83 | imgcode_online.timestamp = time.time() 84 | 85 | current_time = time.time() 86 | if current_time - imgcode_online.timestamp > 60: 87 | imgcode_online.counter = 0 88 | imgcode_online.timestamp = current_time 89 | 90 | imgcode_online.counter += 1 91 | if imgcode_online.counter > 10: 92 | imgcode_online.counter = 0 93 | imgcode_online.timestamp = current_time 94 | return False 95 | 96 | # Convert base64 image to bytes 97 | img_data = base64.b64decode(imgurl.split(",")[-1]) 98 | files = {'image': ('image.jpg', img_data)} 99 | response = requests.post('云函数给你的访问路径url/ocr/file/json', files=files) 100 | 101 | if response.text: 102 | try: 103 | result = json.loads(response.text) 104 | if result['status'] == 200: 105 | print(result['result']) 106 | return result['result'] 107 | elif result['status'] != 200: 108 | time.sleep(10) 109 | return imgcode_online(imgurl) 110 | else: 111 | print(result['msg']) 112 | return 'error' 113 | except json.JSONDecodeError: 114 | print("Invalid JSON received") 115 | return 'error' 116 | else: 117 | print("Empty response received") 118 | return 'error' 119 | ``` 120 | 121 | ## 成功示例: 122 | **ps:抢课成功的实例也类似,基本上只要有人退课你就能抢到** 123 | 124 | 125 | 126 | 127 | 2023-6-23注销测试 128 | 129 | 130 | 131 | 2023-12-28修复成功 132 | 133 | 134 | 135 | ## 郑重声明: 136 | 137 | ### 此程序仅作为技术交流之用,请不要将其用于任何形式的收费行为中 138 | -------------------------------------------------------------------------------- /build_script.py: -------------------------------------------------------------------------------- 1 | import PyInstaller.__main__ 2 | 3 | fake_useragent_data_path = 'xk_spider/data/browsers.json' 4 | 5 | PyInstaller.__main__.run([ 6 | 'xk_spider/run.py', # 替换为你的脚本名 7 | '--onefile', 8 | '--add-data', 'xk_spider/AutoLogin.py;.', 9 | '--add-data', 'xk_spider/GetCourse.py;.', 10 | f'--add-data={fake_useragent_data_path};fake_useragent/data', 11 | '--hidden-import=fake_useragent', 12 | '--hidden-import=concurrent.futures', 13 | '--name', 'run' 14 | ]) 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | selenium == 4.1.0 2 | requests 3 | flask == 3.0.0 4 | ddddocr == 1.5.5 5 | fake_useragent -------------------------------------------------------------------------------- /resource/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidwushi1145/YNU-xk_spider/1d45d88557e8e84511b53bb9c16aea2153a830e9/resource/1.png -------------------------------------------------------------------------------- /resource/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidwushi1145/YNU-xk_spider/1d45d88557e8e84511b53bb9c16aea2153a830e9/resource/2.png -------------------------------------------------------------------------------- /resource/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidwushi1145/YNU-xk_spider/1d45d88557e8e84511b53bb9c16aea2153a830e9/resource/4.png -------------------------------------------------------------------------------- /resource/res1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidwushi1145/YNU-xk_spider/1d45d88557e8e84511b53bb9c16aea2153a830e9/resource/res1.png -------------------------------------------------------------------------------- /resource/res2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidwushi1145/YNU-xk_spider/1d45d88557e8e84511b53bb9c16aea2153a830e9/resource/res2.jpg -------------------------------------------------------------------------------- /resource/res3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidwushi1145/YNU-xk_spider/1d45d88557e8e84511b53bb9c16aea2153a830e9/resource/res3.jpg -------------------------------------------------------------------------------- /resource/rysh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidwushi1145/YNU-xk_spider/1d45d88557e8e84511b53bb9c16aea2153a830e9/resource/rysh.jpg -------------------------------------------------------------------------------- /xk_spider/AutoLogin.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import base64 3 | import json 4 | import threading 5 | import time 6 | from urllib.parse import urlparse, parse_qs 7 | 8 | import requests 9 | from selenium import webdriver 10 | from selenium.common.exceptions import TimeoutException 11 | from selenium.webdriver.chrome.options import Options 12 | from selenium.webdriver.common.by import By 13 | from selenium.webdriver.support import expected_conditions as EC 14 | from selenium.webdriver.support.wait import WebDriverWait 15 | 16 | 17 | class AutoLogin: 18 | def __init__(self, url, path, name='', pswd=''): # 增加自动登录中过验证码功能 19 | self.timer = None 20 | # 设置 Chrome 为无界面模式 21 | chrome_options = Options() 22 | chrome_options.add_argument('--ignore-certificate-errors') 23 | chrome_options.add_argument("--headless") # 启用无界面模式 24 | chrome_options.add_argument('--disable-gpu') # 禁用 GPU 加速,某些系统/版本下需要 25 | chrome_options.add_argument('--window-size=1920x1080') # 指定浏览器分辨率 26 | 27 | # 初始化 WebDriver,使用指定的 Chrome 驱动路径和 Chrome 选项 28 | self.driver = webdriver.Chrome(executable_path=path, options=chrome_options) 29 | # self.driver = webdriver.Chrome(executable_path=path) 30 | self.name = name 31 | self.url = url 32 | self.pswd = pswd 33 | 34 | def start_timer(self): 35 | # 启动一个定时器,在 60 秒后调用 close_driver 方法 36 | self.timer = threading.Timer(60.0, self.close_driver2) 37 | self.timer.start() 38 | 39 | def close_driver(self): 40 | # 关闭浏览器驱动 41 | if self.driver: 42 | self.driver.quit() 43 | self.driver = None 44 | # 停止并清除定时器 45 | if self.timer: 46 | self.timer.cancel() 47 | self.timer = None 48 | 49 | def close_driver2(self): 50 | # 关闭浏览器驱动 51 | if self.driver: 52 | self.driver.quit() 53 | self.driver = None 54 | # 停止并清除定时器 55 | if self.timer: 56 | self.timer.cancel() 57 | self.timer = None 58 | # 如果驱动运行超过60秒,引发一个异常 59 | return False 60 | 61 | def get_params(self): 62 | # 获得必要参数 63 | self.start_timer() 64 | self.driver.get(self.url) 65 | WebDriverWait(self.driver, 15).until(EC.presence_of_element_located((By.ID, 'vcodeImg'))) 66 | # 查找验证码标签 67 | img_tag = self.driver.find_element(By.ID, 'vcodeImg') 68 | src = img_tag.get_attribute('src') 69 | print(src) 70 | # 如果验证码为空 刷新页面 71 | while src == '': 72 | self.driver.refresh() 73 | WebDriverWait(self.driver, 15).until(EC.presence_of_element_located((By.ID, 'vcodeImg'))) 74 | img_tag = self.driver.find_element(By.ID, 'vcodeImg') 75 | img_tag.click() 76 | time.sleep(3) 77 | src = img_tag.get_attribute('src') 78 | # 通过识别接口识别并获取验证码 79 | src = img_to_base64(src) 80 | vcode = imgcode_online(src) 81 | # 输入用户名 密码 验证码 82 | name_ele = self.driver.find_element(By.XPATH, '//input[@id="loginName"]') 83 | name_ele.send_keys(self.name) 84 | pswd_ele = self.driver.find_element(By.XPATH, '//input[@id="loginPwd"]') 85 | pswd_ele.send_keys(self.pswd) 86 | vcode_ele = self.driver.find_element(By.XPATH, '//input[@id="verifyCode"]') 87 | vcode_ele.send_keys(vcode) 88 | # 进行自动登录 89 | login_ele = self.driver.find_element(By.XPATH, '//button[@id="studentLoginBtn"]') 90 | login_ele.click() 91 | time.sleep(1) 92 | flag = 0 93 | # 如果出现验证码错误弹窗 重新获取验证码 94 | while True: 95 | if flag < 3: 96 | error_message = self.driver.find_element(By.XPATH, '//button[@id="errorMsg"]') 97 | error_text = error_message.text 98 | login_ele = self.driver.find_element(By.XPATH, '//button[@id="studentLoginBtn"]') 99 | print(error_text) 100 | if error_text == "验证码不正确": 101 | flag += 1 102 | vcode_ele.clear() 103 | img_tag = self.driver.find_element(By.ID, 'vcodeImg') 104 | img_tag.click() 105 | time.sleep(3) 106 | src = img_tag.get_attribute('src') 107 | src = img_to_base64(src) 108 | vcode = imgcode_online(src) 109 | vcode_ele = self.driver.find_element(By.XPATH, '//input[@id="verifyCode"]') 110 | vcode_ele.send_keys(vcode) 111 | login_ele.click() 112 | time.sleep(1) 113 | elif error_text == "认证失败": 114 | self.close_driver() 115 | return False 116 | else: 117 | break 118 | else: 119 | self.close_driver() 120 | return False 121 | # 点击选课按钮 122 | try: 123 | WebDriverWait(self.driver, 5).until(EC.presence_of_element_located((By.XPATH, '//button[@class="bh-btn ' 124 | 'cv-btn bh-btn-primary ' 125 | 'bh-pull-right"]'))) 126 | # 如果按钮出现,点击按钮 127 | button_ele = self.driver.find_element(By.XPATH, '//button[@class="bh-btn cv-btn bh-btn-primary ' 128 | 'bh-pull-right"]') 129 | button_ele.click() 130 | except TimeoutException: 131 | # 如果按钮没有出现,可以选择忽略,继续运行其他代码 132 | pass 133 | WebDriverWait(self.driver, 15).until(EC.presence_of_element_located((By.XPATH, '//button[@class="bh-btn ' 134 | 'bh-btn bh-btn-primary ' 135 | 'bh-pull-right"]'))) 136 | ok_ele = self.driver.find_element(By.XPATH, '//button[@class="bh-btn bh-btn bh-btn-primary bh-pull-right"]') 137 | ok_ele.click() 138 | time.sleep(1) 139 | try: 140 | start_ele = WebDriverWait(self.driver, 20).until( 141 | EC.presence_of_element_located((By.XPATH, '//button[@id="courseBtn"]')) 142 | ) 143 | self.driver.execute_script("arguments[0].click();", start_ele) 144 | except TimeoutException: 145 | print("在尝试点击时发生超时。") 146 | return False 147 | 148 | if WebDriverWait(self.driver, 8).until(EC.presence_of_element_located((By.ID, 'aPublicCourse'))): 149 | time.sleep(2) # waiting for loading 150 | cookie_lis = self.driver.get_cookies() 151 | cookies = '' 152 | for item in cookie_lis: 153 | cookies += item['name'] + '=' + item['value'] + '; ' 154 | token = self.driver.execute_script('return sessionStorage.getItem("token");') # 暂时无用 155 | batch_str = self.driver. \ 156 | execute_script('return sessionStorage.getItem("currentBatch");').replace('null', 'None').replace( 157 | 'false', 'False').replace('true', 'True') 158 | batch = ast.literal_eval(batch_str) 159 | # 获取当前的网址 160 | current_url = self.driver.current_url 161 | 162 | # 解析 URL 并获取查询参数 163 | parsed_url = urlparse(current_url) 164 | query_params = parse_qs(parsed_url.query) 165 | 166 | # 获取 token 167 | token = query_params.get('token', [None])[0] 168 | 169 | if token is not None: 170 | print("Token found in the URL") 171 | print("Token: {}".format(token)) 172 | else: 173 | print("No token found in the URL") 174 | self.close_driver() 175 | return cookies, batch['code'], token 176 | 177 | else: 178 | print('page load failed') 179 | self.close_driver() 180 | return False 181 | 182 | 183 | # 识别验证码(自己在本地部署或者嫖别人的) 184 | def imgcode_online(imgurl): 185 | if not hasattr(imgcode_online, "counter"): 186 | imgcode_online.counter = 0 187 | if not hasattr(imgcode_online, "timestamp"): 188 | imgcode_online.timestamp = time.time() 189 | 190 | current_time = time.time() 191 | if current_time - imgcode_online.timestamp > 60: 192 | imgcode_online.counter = 0 193 | imgcode_online.timestamp = current_time 194 | 195 | imgcode_online.counter += 1 196 | if imgcode_online.counter > 10: 197 | imgcode_online.counter = 0 198 | imgcode_online.timestamp = current_time 199 | return False 200 | 201 | d = {'data': imgurl} 202 | response = requests.post('http://127.0.0.1:5000/base64img', data=d) 203 | if response.text: 204 | try: 205 | result = json.loads(response.text) 206 | if result['code'] == 200: 207 | print(result['data']) 208 | return result['data'] 209 | elif result['code'] != 200: 210 | time.sleep(10) 211 | return imgcode_online(imgurl) 212 | else: 213 | print(result['msg']) 214 | return False 215 | except json.JSONDecodeError: 216 | print("Invalid JSON received") 217 | return False 218 | else: 219 | print("Empty response received") 220 | return False 221 | 222 | 223 | def img_to_base64(img_url): 224 | response = requests.get(img_url) 225 | if response is None: 226 | return False 227 | img_data = base64.b64encode(response.content).decode('utf-8') 228 | return 'data:image/jpeg;base64,' + img_data 229 | -------------------------------------------------------------------------------- /xk_spider/GetCourse.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import random 3 | import re 4 | import time 5 | 6 | import requests 7 | from requests.exceptions import HTTPError 8 | from requests.utils import dict_from_cookiejar 9 | 10 | 11 | def to_wechat(key, title, string): 12 | url = 'https://sctapi.ftqq.com/' + key + '.send' 13 | dic = { 14 | 'text': title, 15 | 'desp': string 16 | } 17 | requests.get(url, params=dic) 18 | 19 | return title + ':已发送至微信' 20 | 21 | 22 | class GetCourse: 23 | def __init__(self, headers: dict, stdcode, batchcode, driver, url, path, stdCode, pswd): 24 | self.driver = driver 25 | self.headers = headers 26 | self.stdcode = stdcode 27 | self.batchcode = batchcode 28 | self.url = url 29 | self.path = path 30 | self.stdCode = stdCode 31 | self.pswd = pswd 32 | 33 | def judge(self, course_name, teacher, key='', kind=''): 34 | # 人数未满才返回classid 35 | classtype = "XGXK" 36 | if kind == '素选': 37 | kind = 'publicCourse.do' 38 | elif kind == '主修': 39 | kind = 'programCourse.do' 40 | classtype = "FANKC" 41 | elif kind == '体育': 42 | kind = 'programCourse.do' 43 | classtype = "TYKC" 44 | url = 'http://xk.ynu.edu.cn/xsxkapp/sys/xsxkapp/elective/' + kind 45 | 46 | while True: 47 | try: 48 | query = self.__judge_datastruct(course_name, classtype) 49 | r = requests.post(url, data=query, headers=self.headers) 50 | r.raise_for_status() 51 | flag = 0 52 | while not r: 53 | if flag > 2: 54 | to_wechat(key, f'{course_name} 查询失败,请检查失败原因', '线程结束') 55 | return False 56 | print(f'[warning]: jugde()函数正尝试再次爬取') 57 | time.sleep(3) 58 | r = requests.post(url, data=query, headers=self.headers) 59 | try: 60 | setcookie = r.cookies 61 | except KeyError: 62 | setcookie = '' 63 | 64 | if setcookie: 65 | # 将 RequestsCookieJar 对象转换为字典 66 | cookies_dict = dict_from_cookiejar(setcookie) 67 | # 将字典转换为字符串 68 | setcookie_str = '; '.join([f'{k}={v}' for k, v in cookies_dict.items()]) 69 | 70 | # 在字符串中搜索_WEU Cookie 71 | match_weu = re.search(r'_WEU=.+?; ', setcookie_str) 72 | if match_weu: 73 | update_weu = match_weu.group(0) 74 | self.headers['cookie'] = re.sub(r'_WEU=.+?; ', update_weu, self.headers.get('cookie', '')) 75 | else: 76 | print("No _WEU match found") 77 | 78 | # 在字符串中搜索其他Cookie并进行更新 79 | match_jsessionid = re.search(r'JSESSIONID=.+?; ', setcookie_str) 80 | if match_jsessionid: 81 | update_jsessionid = match_jsessionid.group(0) 82 | self.headers['cookie'] = re.sub(r'JSESSIONID=.+?; ', update_jsessionid, 83 | self.headers.get('cookie', '')) 84 | 85 | match_pgv_pvi = re.search(r'pgv_pvi=.+?; ', setcookie_str) 86 | if match_pgv_pvi: 87 | update_pgv_pvi = match_pgv_pvi.group(0) 88 | self.headers['cookie'] = re.sub(r'pgv_pvi=.+?; ', update_pgv_pvi, 89 | self.headers.get('cookie', '')) 90 | 91 | print(f'[current cookie]: {self.headers["cookie"]}') 92 | else: 93 | print("No setcookie found") 94 | 95 | temp = r.text.replace('null', 'None').replace('false', 'False').replace('true', 'True') 96 | res = ast.literal_eval(temp) 97 | 98 | if res['msg'] == '未查询到登录信息': 99 | print('登录失效,请重新登录') 100 | return False 101 | 102 | if kind == 'publicCourse.do': 103 | datalist = res['dataList'] 104 | elif kind == 'programCourse.do': 105 | datalist = res['dataList'][0]['tcList'] 106 | else: 107 | print('kind参数错误,请重新输入') 108 | return False 109 | 110 | for course in datalist: 111 | remain = int(course['classCapacity']) - int(course['numberOfFirstVolunteer']) 112 | if remain > 0 and course['teacherName'] == teacher: 113 | string = f'{course_name} {teacher}:{remain}人空缺' 114 | print(string) 115 | to_wechat(key, f'{course_name} 余课提醒', string) 116 | res = self.post_add(course_name, teacher, classtype, course['teachingClassID'], key) 117 | # 若同一个老师开设多门同样课程,持续抢课 118 | if '该课程与已选课程时间冲突' in res: 119 | continue 120 | if '人数已满' in res: 121 | continue 122 | if '添加选课志愿成功' in res: 123 | return res 124 | return res 125 | 126 | print(f'{course_name} {teacher}:人数已满 {time.ctime()}') 127 | sleep_time = random.randint(3, 10) 128 | time.sleep(sleep_time) 129 | 130 | except HTTPError or SyntaxError: 131 | print('登录失效,请重新登录') 132 | return False 133 | 134 | def post_add(self, classname, teacher, classtype, classid, key): 135 | query = self.__add_datastruct(classid, classtype) 136 | 137 | url = 'http://xk.ynu.edu.cn/xsxkapp/sys/xsxkapp/elective/volunteer.do' 138 | r = requests.post(url, headers=self.headers, data=query) 139 | flag = 0 140 | while not r: 141 | if flag > 2: 142 | to_wechat(key, f'{classname} 有余课,但post未成功', '线程结束') 143 | break 144 | print(f'[warning]: post_add()函数正尝试再次请求') 145 | time.sleep(3) 146 | r = requests.post(url, headers=self.headers, data=query) 147 | flag += 1 148 | 149 | messge_str = r.text.replace('null', 'None').replace('false', 'False').replace('true', 'True') 150 | messge = ast.literal_eval(messge_str)['msg'] 151 | title = '抢课结果' 152 | string = '[' + teacher + ']' + classname + ': ' + messge 153 | to_wechat(key, title, string) 154 | return string 155 | 156 | def __add_datastruct(self, classid, classtype) -> dict: 157 | post_course = { 158 | "data": { 159 | "operationType": "1", 160 | "studentCode": self.stdcode, 161 | "electiveBatchCode": self.batchcode, 162 | "teachingClassId": classid, 163 | "isMajor": "1", 164 | "campus": "05", # 01是东陆的校区代码 165 | "teachingClassType": classtype 166 | } 167 | } 168 | query = { 169 | 'addParam': str(post_course) 170 | } 171 | 172 | return query 173 | 174 | def __judge_datastruct(self, course, classtype) -> dict: 175 | data = { 176 | "data": { 177 | "studentCode": self.stdcode, 178 | "campus": "05", # 01是东陆的校区代码 179 | "electiveBatchCode": self.batchcode, 180 | "isMajor": "1", 181 | "teachingClassType": classtype, 182 | "checkConflict": "2", 183 | "checkCapacity": "2", 184 | "queryContent": course 185 | }, 186 | "pageSize": "10", 187 | "pageNumber": "0", 188 | "order": "" 189 | } 190 | query = { 191 | 'querySetting': str(data) 192 | } 193 | 194 | return query 195 | -------------------------------------------------------------------------------- /xk_spider/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidwushi1145/YNU-xk_spider/1d45d88557e8e84511b53bb9c16aea2153a830e9/xk_spider/__init__.py -------------------------------------------------------------------------------- /xk_spider/api.py: -------------------------------------------------------------------------------- 1 | # 部署方法 2 | # https://cloud.tencent.com/document/product/583/55594#install 3 | import base64 4 | import ddddocr 5 | # pip install ddddocr 6 | # 腾讯云,云函数:pip3 install ddddocr -t ./src 7 | import binascii 8 | from flask import Flask, request, jsonify, render_template 9 | import codecs 10 | import sys 11 | 12 | # 设置网页编码 13 | sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach()) 14 | 15 | app = Flask(__name__) 16 | app.config.update(DEBUG=False) 17 | UPLOAD_FOLDER = 'upload' 18 | app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER 19 | ALLOWED_EXTENSIONS = {'png', 'jpg', 'JPG', 'PNG', 'gif', 'GIF', 'jfif', 'jpeg'} 20 | 21 | # ocr验证码识别初始化 22 | ocr = ddddocr.DdddOcr() 23 | ocr.set_ranges(6) 24 | 25 | # 获取文件后缀 26 | def allowed_file(filename): 27 | return '.' in filename and filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS 28 | 29 | 30 | # 判断是否为base64 31 | def isBase64Img(str_img): 32 | try: 33 | base64_img = str_img.split(',')[1] 34 | return base64.b64decode(base64_img) 35 | except binascii.Error: 36 | return False 37 | 38 | 39 | @app.route('/') 40 | def index(): 41 | return render_template('index.html') 42 | 43 | 44 | # 识别base64图片 45 | @app.route('/base64img', methods=['GET', 'POST']) 46 | def base64img(): 47 | if request.method == 'GET': 48 | src = request.args['data'] 49 | else: 50 | src = request.form['data'] 51 | if isBase64Img(src): 52 | data = src.split(',')[1] 53 | image_data = base64.b64decode(data) 54 | res = ocr.classification(image_data) 55 | if not res: 56 | return jsonify({'code': -404, 'msg': '识别失败'}) 57 | return jsonify({'code': 200, 'data': str(res), 'msg': '识别成功'}) 58 | else: 59 | return jsonify({'code': -300, 'msg': 'base64图片转存失败'}) 60 | 61 | 62 | # 识别上传的图片 63 | @app.route('/up_file', methods=['POST'], strict_slashes=False) 64 | def up_file(): 65 | if request.method == 'POST': # 如果是 POST 请求方式 66 | file = request.files.get('file') # 获取上传的文件 67 | if not file: 68 | return jsonify({'code': -201, 'msg': '没有上传图片'}) 69 | if not allowed_file(file.filename): 70 | return jsonify({'code': -202, 'msg': '文件格式不支持'}) 71 | img = file.stream.read() 72 | res = ocr.classification(img) 73 | if not res: 74 | return jsonify({'code': -404, 'msg': '识别失败'}) 75 | return jsonify({'code': 200, 'data': str(res), 'msg': '识别成功'}) 76 | # 使用 GET 方式请求页面时或是上传文件失败时返回上传文件的表单页面 77 | return jsonify({'code': -200, 'msg': '图片上传失败'}) 78 | 79 | 80 | @app.errorhandler(400) 81 | def error(e): 82 | print(e) 83 | return jsonify({'code': -400, 'msg': str(e)}) 84 | 85 | 86 | @app.errorhandler(404) 87 | def page_not_found(e): 88 | return jsonify({'code': -3000, 'msg': '非法请求'}) 89 | 90 | 91 | if __name__ == '__main__': 92 | app.run(host='127.0.0.1', port=5000) 93 | -------------------------------------------------------------------------------- /xk_spider/run.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from concurrent.futures import ThreadPoolExecutor, as_completed 4 | 5 | from fake_useragent import UserAgent 6 | 7 | from xk_spider.AutoLogin import AutoLogin 8 | from xk_spider.GetCourse import GetCourse 9 | 10 | if hasattr(sys, '_MEIPASS'): 11 | data_path = os.path.join(sys._MEIPASS, 'data', 'browsers.json') 12 | else: 13 | data_path = os.path.join(os.path.dirname(__file__), 'data', 'browsers.json') 14 | # 程序全自动运行,如果出现bug请提issue 15 | ua = UserAgent() 16 | headers = { 17 | 'User-Agent': ua.random 18 | } 19 | url = 'http://xk.ynu.edu.cn/' 20 | stdCode = '' # 在''中填入你的学号 21 | pswd = '' # 填你的密码 22 | key = '' # 填你在server酱上获取到的key 23 | path = '' # 填写你的chromedriver路径,如 '/usr/local/bin/chromedriver 或 C:/Program Files/Google/Chrome/Application/chromedriver' 24 | # 下面这个列表填你想查询的 素选课 ,以 ['课程名称', '授课老师'], 的格式填,注意最后有一个 英文 逗号 25 | 26 | # ----- 注意,课程名称要保证在选课页面你能用这个名称搜得出来 !!! ----- 27 | 28 | publicCourses = [ 29 | # ['大学生创新创业教育', '何鸣皋'], # 这是个测试用例,可以先不修改直接运行看看是否成功,如果不小心抢到了自己手动退掉就好 30 | ] 31 | 32 | '''下面这个列表填你想查询的体育课,包括必修和选修,格式填写同上 33 | 体育课格式如下,请确保完全按照体育课程名填写,有的课程名没有括号''' 34 | peCourses = [ 35 | # ['羽毛球(四)', '范丽霞'], 36 | ] 37 | 38 | # 下面这个列表填你想查询的 主修课,包括必修和选修,格式填写同上 39 | programCourse = [ 40 | # ['大学生创新创业教育', '段连丽'], 41 | ] 42 | 43 | '''以上两个列表理论上可以接受任意数量的课程,填写模板如下。但数量最好不要超过你CPU的核心数(一般电脑都在4核以上) 44 | programCourse = [ 45 | ['课程1', '老师1'], 46 | ['课程2', '老师2'], 47 | ['课程3', '老师3'], 48 | ] 49 | ''' 50 | 51 | while True: 52 | try: 53 | al = AutoLogin(url, path, stdCode, pswd) 54 | params = al.get_params() 55 | if not params: 56 | continue 57 | headers['cookie'], batchCode, Token = params 58 | headers['Token'] = Token 59 | headers['Authorization'] = 'Bearer ' + Token 60 | 61 | gc = GetCourse(headers, stdCode, batchCode, al.driver, url, path, stdCode, pswd) 62 | 63 | ec = ThreadPoolExecutor() 64 | taskList = [] 65 | for course in publicCourses: 66 | taskList.append(ec.submit(gc.judge, course[0], course[1], key, kind='素选')) 67 | for course in programCourse: 68 | taskList.append(ec.submit(gc.judge, course[0], course[1], key, kind='主修')) 69 | for course in peCourses: 70 | taskList.append(ec.submit(gc.judge, course[0], course[1], key, kind='体育')) 71 | 72 | for future in as_completed(taskList): 73 | result = future.result() 74 | print(result) 75 | if not result: 76 | break # If the judge method returns False, break the loop to start a new login process 77 | except Exception as e: 78 | print(f"An error occurred: {e}, restarting the login process.") 79 | --------------------------------------------------------------------------------