├── 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 | --------------------------------------------------------------------------------