├── 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 | 
65 |
66 | 高级配置拉满
67 |
68 | 
69 |
70 | 然后测试即可
71 |
72 | 
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 |
--------------------------------------------------------------------------------