├── README.md
└── main.py
/README.md:
--------------------------------------------------------------------------------
1 | # 项目名称 (基于python的签到小助手)
2 |
3 | > [最开始写出来是让自己个室友能睡个好觉,后面被宣传出去了才开始正儿八经弄。]
4 |
5 | ---
6 |
7 | ## 📝 作者的话与项目状态
8 |
9 | 大家好!关于大家关心的**二维码直签**功能,目前开发优先级不高,暂时没有加入开发计划。
10 |
11 | 另外,请注意:平台(XXT)在 **2025年4月前后** 对部分接口进行了调整,这可能导致本项目的某些功能受到限制或表现不稳定。鉴于此,以及社区中存在的一些不友好的行为(如接口盗用),后续的功能更新频率可能会有所放缓。感谢大家的理解与支持!
12 |
13 | ---
14 |
15 | ## 🌟 生态与社区伙伴
16 |
17 | 探索更多由朋友们维护的优秀项目和服务:
18 | * 🌐 **不想去上课**:
19 | * *特色:小程序 + exe自动签,随时随地签到*
20 | * 🌐 **御坂网络**: [https://cx.micono.eu.org/](https://cx.micono.eu.org/)
21 | * *特色:小程序 + 网页,随时随地签到*
22 | * 💎 **上签**: [https://api2.function.icu/](https://api2.function.icu/)
23 | * *特色:致力于为富哥富姐带来远超WAADRI的体验,将白嫖用户拦在外面给付费用户腾空间,24小时为你服务*
24 | * 🐈 **修猫的学习助手**: [https://xiucat.top/](https://xiucat.top/)
25 | * *特色:小程序 + 网页,便捷签到*
26 | * 📶 **河南省WiFi研究所**: [https://www.waadri.top/](https://www.waadri.top/)
27 | * *特色:小程序 + 网页,签到好帮手*
28 |
29 | ---
30 |
31 | ## 🛠️ 功能对比
32 |
33 | | 功能模块 | 具体功能 | ✅ 开源版 (Free) | ✅ 手动挡 (已经暂停更新功能大部分失效) | ✨ 自动挡 (Auto Premium) |
34 | | :-------------- | :--------------- | :-------------------- | :--------------------- | :----------------------- |
35 | | 📌 **签到功能** | 📍 位置签到 | ❌ 仅支持手动输入 | ✅ 自动获取教师位置 | ✅ 自动获取教师位置 |
36 | | | 📸 拍照签到 | ⚠️ 手动上传任意图片 | ⚠️ 手动上传任意图片 | ✅ **智能抓取同学照片** |
37 | | | ✌️ 手势签到 | ❌ 不支持 | ✅ 自动解析手势轨迹 | ✅ 自动解析手势轨迹 |
38 | | | 🔢 签到码签到 | ❌ 不支持 | ✅ 实时获取签到码 | ✅ 实时获取签到码 |
39 | | | 🔳 二维码签到 | ❌ 不支持 | ❌ 不支持 | ✅ **解除限制自动签**¹ |
40 | | ⚙️ **系统特性** | 🧩 滑块验证码 | ❌ 不支持 | ✅ 自动识别破解 | ✅ 自动识别破解 |
41 | | | 👆 指纹/面部验证² | ❌ 不支持 | ✅ 需联系管理员同步 | ✅ 需联系管理员同步 |
42 | | 👥 **多账号** | 支持数量 | 💡 通过多开实现 (无限制) | 💡 通过多开实现 (无限制) | 💡 通过多开实现 (无限制) |
43 |
44 | **注释:**
45 | 1. 二维码自动签到需搭配特定的扫码小程序使用。
46 | 2. 指纹/面部验证功能可能依赖于平台接口,具体支持情况请咨询。
47 |
48 | ---
49 |
50 | ## 🚀 使用方式对比
51 |
52 | | 版本类型 | 启动时机 | 运行模式 | 主要优势 |
53 | | :------- | :------------------- | :------------------- | :----------------- |
54 | | 开源版 | 📅 签到发布后启动 | 👀 被动监听等待 | 免费,代码开放 |
55 | | 闭源版 | ⏰ 可提前部署运行 | 🤖 全自动即时响应 | 功能全,自动化程度高 |
56 |
57 | ---
58 |
59 | ## 📢 加入交流群 & 获取帮助
60 |
61 | 🔔 **福利活动 & 技术支持**:
62 | 欢迎加入 **签到交流 QQ 群: 736521378**
63 |
64 | **群内福利:**
65 | * 💬 与开发者和其他用户进行技术交流,分享使用心得
66 | * 💡 提出功能建议,参与项目改进
67 | * 🐛 反馈 Bug,享受优先修复权
68 |
69 |
70 |
71 |
扫码加入 QQ 交流群
72 |
73 |
74 | **配套扫码小程序 (用于闭源版二维码签到):**
75 |
76 |
77 |
78 |
扫码体验小程序
79 |
80 |
81 | ---
82 |
83 | ## 📜 版本与声明
84 |
85 | 1. **开源版本**:
86 | * 代码已在本仓库发布。
87 | * 欢迎开发者在此基础上进行学习、修改和扩展功能。
88 | 2. **闭源版本**:
89 | * 采用订阅制服务模式。
90 | * 提供更完整的功能集和更及时的技术支持。
91 | 3. **使用目的**:
92 | * 本系统及其衍生版本仅供个人学习和技术交流使用。
93 | * **严禁将本项目的任何部分用于商业用途或非法活动。**
94 | * 使用者应对自己的行为负责,作者不承担任何因使用不当造成的后果。
95 |
96 | ---
97 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import time
4 | import json
5 | import pickle
6 | import re
7 | import requests
8 | import base64
9 | from bs4 import BeautifulSoup
10 | import tkinter as tk
11 | from tkinter import filedialog
12 | from colorama import init, Fore
13 | import tempfile
14 | import urllib.parse
15 | from datetime import datetime
16 | from Crypto.Cipher import AES
17 | from Crypto.Util.Padding import pad
18 | from Crypto.Random import get_random_bytes
19 | init() # 初始化colorama
20 | header = {
21 | 'Accept-Encoding' : 'gzip, deflate',
22 | 'Accept-Language' : 'zh-CN,zh;q=0.9',
23 | 'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
24 | 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36'}
25 |
26 | ua={
27 | 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36'
28 | }
29 |
30 | coursedata = []
31 | coursedatas = []
32 | activates = []
33 |
34 | def is_running_from_temp_directory():
35 | # 获取可执行文件的绝对路径
36 | exe_path = os.path.abspath(sys.executable)
37 | # 获取系统的临时目录路径
38 | temp_directory = tempfile.gettempdir()
39 |
40 | # 检查可执行文件的路径是否以系统临时目录开头
41 | if exe_path.startswith(temp_directory):
42 | return True
43 | return False
44 |
45 | # 保存用户凭证到本地文件
46 | def save_credentials(username, password, filename='账号信息'):
47 | with open(filename, 'wb') as f:
48 | # 将用户名和密码保存为pickle格式
49 | pickle.dump({'username': username, 'password': password}, f)
50 |
51 | # 从本地文件加载用户凭证
52 | def load_credentials(filename='账号信息'):
53 | if os.path.exists(filename):
54 | try:
55 | with open(filename, 'rb') as f:
56 | credentials = pickle.load(f)
57 | return credentials['username'], credentials['password']
58 | except (EOFError, KeyError):
59 | print("凭证文件为空或格式不正确。")
60 | else:
61 | print("凭证文件不存在。")
62 | return None, None
63 | def load_coursedata(filename='coursedata.json'):
64 | if os.path.exists(filename):
65 | try:
66 | with open(filename, 'r', encoding='utf-8') as f:
67 | coursedata = json.load(f)
68 | return coursedata
69 | except (json.JSONDecodeError, KeyError) as e:
70 | print(f"课程文件读取错误:{e}")
71 | else:
72 | print("课程文件不存在。")
73 | return None
74 | # 弹出文件选择对话框,让用户选择文件
75 | def select_file():
76 | # 创建 Tkinter 根窗口
77 | root = tk.Tk()
78 | # 隐藏根窗口
79 | root.withdraw()
80 | # 确保主窗口不会被关闭
81 | root.update()
82 | # 定义图片文件的扩展名
83 | file_types = [
84 | ('Image files', '*.jpg *.jpeg *.png *.gif'),
85 | ('All files', '*.*')
86 | ]
87 | # 弹出文件选择对话框,只显示图片文件类型
88 | file_path = filedialog.askopenfilename(filetypes=file_types)
89 | # 完成后关闭根窗口
90 | root.destroy()
91 | return file_path
92 |
93 | # 登录函数,使用用户名和密码进行登录
94 | def login(username, password):
95 |
96 | # 创建会话对象
97 | session = requests.Session()
98 | # 登录API URL
99 | url = 'https://passport2-api.chaoxing.com/v11/loginregister'
100 | # 构造登录请求数据
101 | data = {
102 | "cx_xxt_passport": "json",
103 | "roleSelect": "true",
104 | "uname": username,
105 | "code": password,
106 | "loginType": "1",
107 | }
108 | password=urllib.parse.quote(password)
109 | # 发送登录请求
110 | response = session.get(url, params=data,headers=header,verify=True,allow_redirects=False)
111 | # 解析响应结果
112 | account = response.json()
113 | mes = account.get('mes')
114 | return mes
115 |
116 | # 登录函数,提交POST请求进行登录
117 | def login_post(username, password, schoolid=None):
118 | # 创建会话对象
119 | session = requests.Session()
120 | password=urllib.parse.quote(password)
121 | # 发送登录请求
122 | r = session.post('http://passport2.chaoxing.com/api/login?name={}&pwd={}&schoolid={}&verify=0'.format(username, password, schoolid),headers=header,verify=True, allow_redirects=False)
123 | # 解析响应结果
124 | name = json.loads(r.text)['realname']
125 | uid = json.loads(r.text)['uid']
126 | schoolid = json.loads(r.text)['schoolid']
127 | return session, name, schoolid, uid
128 |
129 | # 获取用户PUID
130 | def get_puid():
131 | # 请求PUID API URL
132 | url = 'https://sso.chaoxing.com/apis/login/userLogin4Uname.do'
133 | # 发送请求并解析响应
134 | response = session.get(url, headers=header,verify=True, allow_redirects=False)
135 | data = response.json()
136 | try:
137 | puid = data["msg"]["puid"]
138 | return puid
139 | except KeyError:
140 | print("未能获取到puid,响应数据可能不包含'msg'键,或者'msg'键的值不包含'puid'。")
141 | # 打印出响应的JSON数据来帮助调试
142 | print(data)
143 | return None
144 |
145 | # 获取Token
146 | def Token():
147 | # Token API URL
148 | url = 'https://pan-yz.chaoxing.com/api/token/uservalid'
149 | # 发送请求并解析响应
150 | response = session.get(url, headers=header,verify=True, allow_redirects=False)
151 | data = response.json()
152 | return data["_token"]
153 |
154 | # 上传文件对象
155 | def obj(token, puid, file):
156 | # 构造文件对象
157 | files = {
158 | "file": ("file.png", open(file, "rb")) # 打开文件用于读取二进制数据
159 | }
160 | # 构造请求数据
161 | data = {
162 | "puid": puid
163 | }
164 | # 发送文件上传请求
165 | u = session.post('https://pan-yz.chaoxing.com/upload?_token={}'.format(token), data=data, files=files, headers=header,verify=True, allow_redirects=False)
166 | # 解析响应
167 | r = json.loads(u.text)
168 | if r['result']:
169 | object_id = r['objectId'] # 提取objectId
170 | print(f'图片上传成功,objectId: {object_id}')
171 | return object_id
172 | else:
173 | print(f'图片上传失败,页面提示: {r["msg"]}')
174 | return None
175 |
176 | # 获取课程列表
177 | def get_data():
178 | url1='https://mooc1-api.chaoxing.com/mycourse/backclazzdata?view=json&rss=1'
179 | response1 = session.get(url1, headers=header, verify=True, allow_redirects=False)
180 | data = response1.json()
181 | course_details = []
182 |
183 | # 遍历每个课程条目
184 | if 'channelList' in data:
185 | for item in data['channelList']:
186 | group_id = item['key'] # 获取 group ID
187 | course_data = item['content'].get('course', {}).get('data', [])
188 | if course_data: # 确保课程数据存在
189 | for course in course_data:
190 | course_id = course.get('id', '未提供课程ID') # 获取 courseId
191 | course_name = course.get('name', '未提供课程名称') # 获取课程名称
192 | course_details.append({
193 | 'name': course_name, # 课程组名称
194 | 'classid': group_id, # 课程组 ID
195 | 'courseid': course_id # 课程 ID
196 | })
197 | with open('coursedata.json', 'w', encoding='utf-8') as file:
198 | json.dump(course_details, file, indent=4, ensure_ascii=False)
199 |
200 | def selected_course(coursedata):
201 | for index, course in enumerate(coursedata):
202 | print(f"{index + 1}: {course['name']}")
203 |
204 | # 用户选择课程
205 | course_index = int(input("请输入课程的序号来获取详细信息: ")) - 1
206 | if 0 <= course_index < len(coursedata):
207 | selected_course = coursedata[course_index]
208 | return selected_course
209 | else:
210 | print("输入的序号无效!")
211 |
212 | # 执行预签到
213 | def YQD(aid, courseId, classId, uid):
214 | # 预签到API URL
215 | url = 'https://mobilelearn.chaoxing.com/newsign/preSign'
216 | # 构造请求数据
217 | data = {
218 | 'activePrimaryId': aid,
219 | 'courseId': courseId,
220 | 'classId': classId,
221 | 'uid': uid,
222 | 'appType': '15',
223 | 'general': '1',
224 | 'sys': '1',
225 | 'ls': '1',
226 | 'tid': '',
227 | 'ut': 's',
228 | }
229 | # 发送请求并解析响应
230 | r = session.get(url, params=data, headers=header,verify=True, allow_redirects=False)
231 | # 使用BeautifulSoup解析HTML
232 | soup = BeautifulSoup(r.text, 'html.parser')
233 | # 查找id为'statuscontent'的标签
234 | status_content = soup.find('h1', id='statuscontent')
235 | # 如果找到标签,提取标签的文本内容;如果没有找到,返回空字符串
236 | is_ok = status_content.get_text(strip=True) if status_content else None
237 | return is_ok
238 | def aes_encrypt(mode='CBC'):
239 | plaintext = os.urandom(16).hex()
240 | key = os.urandom(32).hex()
241 | key = key.encode('utf-8')[:32]
242 | iv = get_random_bytes(16)
243 | cipher = AES.new(key, AES.MODE_CBC, iv)
244 | ciphertext = cipher.encrypt(pad(plaintext.encode(), AES.block_size))
245 | ciphertext_b64 = base64.b64encode(ciphertext).decode('utf-8')
246 | return ciphertext_b64
247 | # 执行签到
248 | def QD(aid, uid, name, schoolid, validate=None,sign_code=None, latitude=None, longitude=None, address=None, objectId=None, enc=None):
249 | # 签到API URL
250 | url = 'https://mobilelearn.chaoxing.com/pptSign/stuSignajax'
251 | # 构造请求数据
252 | data = {
253 | "activeId": aid,
254 | "uid": uid,
255 | "fid": schoolid,
256 | "name": name,
257 | "signCode": sign_code,
258 | "enc": enc,
259 | "ifTiJiao": "1",
260 | "latitude": latitude,
261 | "longitude": longitude,
262 | 'address': address,
263 | 'objectId': objectId,
264 | "ifTiJiao" : "1",
265 | "vpProbability":0,
266 | "vpStrategy" : "",
267 | "deviceCode": aes_encrypt(),
268 | 'validate':validate
269 | }
270 | # 使用POST请求发送数据并打印响应
271 | r = session.post(url, data=data, headers=header,verify=True, allow_redirects=False)
272 | print(r.text)
273 |
274 | # 获取活动列表
275 |
276 | def active_get(fid, courseId, classId):
277 | url = f'https://mobilelearn.chaoxing.com/v2/apis/active/student/activelist?fid={fid}&courseId={courseId}&classId={classId}'
278 | response = session.get(url, headers=header, verify=True, allow_redirects=False)
279 | if response.status_code == 200:
280 | data = response.json() # 解析响应为JSON
281 | active_list = data.get('data', {}).get('activeList', [])
282 | # 正则表达式匹配 "YYYY-MM-DD HH:MM:SS" 或 "MM-DD HH:MM" 格式
283 | time_pattern = re.compile(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}|\d{2}-\d{2} \d{2}:\d{2}')
284 | # 提取需要的信息,仅当 otherId 不为空且 nameFour 不匹配日期时间格式时
285 | extracted_data = []
286 | for activity in active_list:
287 | if activity.get('otherId') and not time_pattern.match(activity.get('nameFour', '')):
288 | extracted_info = {
289 | 'otherId': activity['otherId'],
290 | 'id': activity['id'],
291 | 'nameFour': activity['nameFour'],
292 | 'nameOne': activity['nameOne']
293 | }
294 | extracted_data.append(extracted_info)
295 |
296 | return extracted_data
297 | else:
298 | # 处理错误的情况
299 | print(f"Error: HTTP {response.status_code}")
300 | return None
301 |
302 | def display_activities(fid, courseId, classId):
303 | activities = active_get(fid, courseId, classId)
304 | if activities is not None:
305 | os.system('cls' if os.name == 'nt' else 'clear')
306 | print("\n可用的活动列表:")
307 | for index, activity in enumerate(activities):
308 | print(f"{index + 1}. {activity['nameFour']} ({activity['nameOne']})")
309 | print("0. 返回到课程选择")
310 | # 获取用户输入
311 | try:
312 | choice = int(input("\n请输入你需要签到的活动序号:")) - 1
313 | if choice == -1:
314 | return None # 用户选择返回
315 | elif 0 <= choice < len(activities):
316 | return activities[choice]
317 | else:
318 | print("选择无效,请重试。")
319 | except ValueError:
320 | print("输入错误,请输入数字。")
321 | else:
322 | print("没有可用的活动或发生了错误。")
323 | return None
324 |
325 |
326 | # 执行刷新操作
327 | def SX(aid):
328 | # 刷新API URL
329 | url = f'https://mobilelearn.chaoxing.com/v2/apis/active/getPPTActiveInfo?activeId={aid}&duid=&denc='
330 | # 发送请求并解析响应
331 | r = session.get(url, headers=header,verify=True, allow_redirects=False)
332 | if r.status_code == 200:
333 | data = r.json()
334 | if data.get("result") == 1 and "data" in data and "otherId" in data["data"]:
335 | otherId = data["data"]["otherId"]
336 | if otherId == 0:
337 | return otherId, data["data"]["ifphoto"]
338 | else:
339 | return otherId, None
340 | return None, None
341 |
342 |
343 | # 手动输入经纬度坐标
344 | def input_coordinates(prompt):
345 | while True:
346 | try:
347 | coordinates_input = input(prompt)
348 | latitude, longitude = [float(coord.strip()) for coord in coordinates_input.split(',')]
349 | return latitude, longitude
350 | except ValueError:
351 | print(Fore.RED + "输入格式不正确或不是有效的数字,请按照 '纬度,经度' 的格式重新输入,例如 '116.403514,39.921714'。" + Fore.RESET)
352 |
353 |
354 |
355 |
356 |
357 | if __name__ == "__main__":
358 | if is_running_from_temp_directory():
359 | print(Fore.RED + "检测到程序可能正在从压缩包中运行,请解压后再使用。"+ Fore.RESET)
360 | input()
361 | sys.exit()
362 | else:
363 | print("程序正常启动。")
364 | # 格式化当前时间
365 | formatted_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
366 | attempts = 0
367 | max_attempts=3
368 | # 尝试加载保存的凭证
369 | while True:
370 | # 尝试加载保存的凭证
371 | user, pwd = load_credentials()
372 |
373 | if user and pwd:
374 | # 使用保存的凭证尝试登录
375 | mes= login(user, pwd)
376 | if mes != "验证通过": # 假设mes为"验证通过"表示登录成功
377 | print(f'登录失败,返回信息:{mes}')
378 | attempts += 1
379 | if attempts >= max_attempts:
380 | input('密码错误3次,按任回车退出。')
381 | sys.exit()
382 | continue
383 | else:
384 | session, name, schoolid, uid = login_post(user, pwd)
385 | print("登录成功!")
386 | time.sleep(1)
387 | os.system('cls' if os.name == 'nt' else 'clear')
388 | break # 登录成功,退出循环
389 | else:
390 | # 如果没有保存的凭证或者登录失败,提示用户输入新的凭证
391 | user = input('请输入账号:')
392 | pwd = input('请输入密码:')
393 | mes = login(user, pwd)
394 | if mes != "验证通过": # 假设mes为"验证通过"表示登录成功
395 | print(f'登录失败,返回信息:{mes}')
396 | continue # 登录失败,继续循环
397 | else:
398 | save_credentials(user, pwd)
399 | session, name, schoolid, uid = login_post(user, pwd)
400 | print("登录成功!")
401 | time.sleep(1)
402 | os.system('cls' if os.name == 'nt' else 'clear')
403 | break # 登录成功,退出循环
404 | print('等待课程数据加载完成...')
405 | get_data()
406 | while True:
407 | os.system('cls' if os.name == 'nt' else 'clear')
408 | coursedata = load_coursedata()
409 | if coursedata is None:
410 | print("加载课程数据失败。")
411 | continue # 如果数据加载失败,重新开始循环
412 |
413 | course_selection = selected_course(coursedata)
414 | if course_selection is None:
415 | print("未选择有效课程。")
416 | continue # 如果没有有效的课程,重新开始循环
417 |
418 | selected_activity = display_activities(schoolid, course_selection['courseid'], course_selection['classid'])
419 | if selected_activity is None:
420 | continue # 如果用户选择返回,重新开始循环
421 |
422 | # 从这点开始,selected_activity 已经被确认不为 None
423 | other_id = selected_activity['otherId']
424 | if selected_activity['id'] is not None:
425 | pas = YQD(selected_activity['id'], course_selection['courseid'], course_selection['classid'], uid)
426 | if pas != "签到成功":
427 | other_id, ifp_id = SX(selected_activity['id']) # 调用SX函数获取otherId和ifp_id
428 |
429 | # 根据other_id的值执行不同的签到流程
430 | if other_id in [3, 5]:
431 | print('当前为手势或验证码签到')
432 | print('签到码&手势为:',None)
433 | QD(selected_activity['id'], uid, name, schoolid,None,None)
434 | elif other_id == 4:
435 | print('当前为位置签到')
436 | print(Fore.RED + "未能成功获取到位置信息,请手动输入。" + Fore.RESET)
437 | address = input('位置名:')
438 | longitude, latitude = input_coordinates('可以前往https://api.map.baidu.com/lbsapi/getpoint/获取,\n请输入纬度和经度,用逗号分隔(例如 106.672333,30.467109):')
439 | QD(selected_activity['id'], uid, name, schoolid, None, None, latitude, longitude, address)
440 | elif other_id == 0:
441 | if ifp_id == 1:
442 | print('当前为拍照签到')
443 | file = select_file()
444 | jpg_a = obj(Token(), get_puid(), file=file)
445 | QD(selected_activity['id'], uid, name, schoolid, None, objectId=jpg_a)
446 | else:
447 | print('当前为普通签到')
448 | QD(selected_activity['id'], uid, name, schoolid, None)
449 | elif other_id == 2:
450 | print(Fore.RED + "暂不支持的签到类型。" + Fore.RESET)
451 | else:
452 | print(Fore.RED + "未知类型活动。" + Fore.RESET)
453 | else:
454 | print("返回类型:", pas)
455 | input("按回车键返回课程列表。")
456 |
--------------------------------------------------------------------------------