├── .gitignore ├── LICENSE ├── README.md ├── docs_requirement.txt ├── example ├── README.txt ├── student │ ├── download_hw_resources.py │ ├── errorbook_templates.html │ ├── get_errorbooks.py │ ├── get_mark.py │ ├── get_weight_mark.py │ └── 获取成绩gui │ │ ├── get_scores.py │ │ ├── ui_login.py │ │ └── ui_mainWindow.py └── teacher │ └── get_paper.py ├── setup.py └── zhixuewang ├── __init__.py ├── account.py ├── exceptions.py ├── models.py ├── session.py ├── student ├── __init__.py ├── student.py └── urls.py ├── teacher ├── __init__.py ├── models.py ├── teacher.py └── urls.py ├── tools ├── __init__.py └── datetime_tool.py └── urls.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Distribution / packaging 7 | .Python 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | wheels/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | MANIFEST 24 | 25 | # Unit test / coverage reports 26 | htmlcov/ 27 | .tox/ 28 | .coverage 29 | .coverage.* 30 | .cache 31 | nosetests.xml 32 | coverage.xml 33 | *.cover 34 | .hypothesis/ 35 | .pytest_cache/ 36 | 37 | .idea 38 | .vscode 39 | user 40 | test* 41 | venv/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 anwenhu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zhixuewang-python 2 | 3 | **注意: 由于登录接口变动, 如果只有学生账号, 该项目与智学网网页版并无二致, 此项目将大概率不会再有关于接口的更新** 4 | 5 | ![](https://img.shields.io/badge/License-MIT-blue) ![](https://img.shields.io/badge/Python-3+-green) ![](https://img.shields.io/pypi/v/zhixuewang) 6 | 7 | ## 安装 8 | 9 | ### 使用 pip 安装(推荐) 10 | ```bash 11 | pip install zhixuewang 12 | ``` 13 | ### 下载 源码 安装 14 | ```bash 15 | git clone https://github.com/anwenhu/zhixuewang 16 | cd zhixuewang 17 | python setup.py install 18 | ``` 19 | 20 | 21 | 22 | ## 简单示例 23 | ### playwright登录(python3.7+) 24 | #### 安装依赖 25 | ``` 26 | playwright install chromium 27 | ``` 28 | #### python代码 29 | ```python 30 | from zhixuewang.account import login_playwright 31 | 32 | zxw = login_playwright(您的智学网账号, 您的智学网密码) 33 | # 然后通过浏览器完成人机验证 34 | 35 | print(zxw.get_self_mark()) 36 | ``` 37 | ### 你也可以手动获取cookie登录 38 | #### python代码 39 | ```python 40 | # from zhixuewang import login_cookie 41 | from zhixuewang.account import login_cookie 42 | 43 | # zxw = login(您的智学网账号, 您的智学网密码) 44 | # 因为智学网接口变动暂时失效,请先使用cookie登录方式 45 | # 复制的cookie字符串 46 | cookie_string = "xxx" 47 | 48 | # 将cookie字符串转换为字典 49 | cookies = dict(item.split("=") for item in cookie_string.split("; ")) 50 | zxw = login_cookie(cookies) 51 | 52 | print(zxw.get_self_mark()) 53 | ``` 54 | #### cookie可以在登录智学网网页端后用以下js书签获取 55 | ```javascript 56 | javascript:(function(){function getCookies(){return document.cookie;}function copyToClipboard(text){const textarea=document.createElement('textarea');textarea.value=text;document.body.appendChild(textarea);textarea.select();document.execCommand('copy');document.body.removeChild(textarea);}const cookies=getCookies();copyToClipboard(cookies);alert('Cookies 已复制到剪切板!');})(); 57 | ``` 58 | ### 结果(仅供参考) 59 | ``` 60 | 您的名字-考试名称 61 | 语文: 121.0 62 | 数学: 121.0 63 | 英语: 137.5 64 | 理综: 277.0 65 | 物理: 101.0 66 | 化学: 93.0 67 | 生物: 83.0 68 | 总分: 656.5 69 | ``` 70 | 71 | **更多高级功能请[查看文档](https://zxdoc.risconn.com)。** 72 | 73 | 74 | 75 | ## 问题和建议 76 | 77 | 如果您在使用的过程中遇到任何问题,或是有任何建议: 78 | 79 | ① 加入QQ群进行讨论:862767072(备注:**GitHub智学网库**); 80 | 81 | ② 前往 [Issues](https://github.com/anwenhu/zhixuewang/issues) 进行提问; 82 | 83 | ③ 如果您想直接贡献代码,欢迎直接 [Pull requests](https://github.com/anwenhu/zhixuewang-python/pulls)。 84 | 85 | 如果有其它不常见的功能需求, 可以前往 [MasterYuan418/zxext](https://github.com/MasterYuan418/zxext) 查找或提出。 86 | 87 | ## Star History 88 | 89 | [![Star History Chart](https://api.star-history.com/svg?repos=anwenhu/zhixuewang-python&type=Date)](https://star-history.com/#anwenhu/zhixuewang-python&Date) 90 | -------------------------------------------------------------------------------- /docs_requirement.txt: -------------------------------------------------------------------------------- 1 | requests 2 | playwright -------------------------------------------------------------------------------- /example/README.txt: -------------------------------------------------------------------------------- 1 | student文件夹内所有代码均需要学生账号 2 | teacher文件夹内所有代码均需要教师账号 3 | -------------------------------------------------------------------------------- /example/student/download_hw_resources.py: -------------------------------------------------------------------------------- 1 | # 自动下载智学网作业资源 2 | # 每分钟检测一次是否有新作业生成, 若有则下载到目录中 3 | 4 | from zhixuewang import login_student 5 | import time 6 | 7 | username = "" # 智学网账号 8 | password = "" # 智学网密码 9 | path = "" # 下载到哪个目录 10 | 11 | zxw = login_student(username, password) 12 | ids = [] 13 | while True: 14 | hws = zxw.get_homeworks() 15 | for homework in hws: 16 | if homework.id not in ids: 17 | for resource in zxw.get_homework_resources(homework): 18 | resource.download(path) 19 | ids.append(homework.id) 20 | time.sleep(60) 21 | -------------------------------------------------------------------------------- /example/student/errorbook_templates.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 成长手册 6 | 7 | 8 | 40 | 41 |
42 |
43 |
44 | 错题本
45 |
46 | 47 |
48 | 姓名:{{model.name}}
49 | 考试名称:{{model.examName}}
50 | 考试科目:{{model.subjectName}}
51 | 班级排名:{{model.rank}}
52 |
53 |
54 |
55 |
56 |
57 | 58 |
59 |
60 | 全部错题
61 |
62 | {% for errorbook in model.errorbooks %} 63 |
64 | {{loop.index}}. 65 | 错题来源:{{errorbook.topic_source_paper_name}}; 66 | 班级正确率:{{errorbook.class_score_rate}} 67 |
68 | 69 |
70 | 【错题订正】: 71 |
72 |
73 |
74 |
75 | {% endfor %} 76 |
77 | 78 |
79 |
80 | 参考答案
81 |
82 | {% for errorbook in model.errorbooks %} 83 | {{loop.index}}. 84 | 答案:
85 | 分析:
86 |
87 |
88 |
89 | {% endfor %} 90 |
91 |
92 | 93 | 94 | -------------------------------------------------------------------------------- /example/student/get_errorbooks.py: -------------------------------------------------------------------------------- 1 | # 获取智学网错题本 2 | # TODO: 字体大小不一致 3 | # 以下代码参考: https://github.com/RICHARDCJ249/zhixue_errorbook 4 | 5 | from zhixuewang import login_student 6 | import jinja2 7 | import os 8 | import datetime 9 | from zhixuewang.account import load_account 10 | import subprocess 11 | 12 | WKHTMLTOPDF_PATH = r'wkhtmltopdf.exe' # wkhtmltopdf 地址 13 | if not os.path.exists("wkhtmltopdf.exe"): 14 | print("请在网上下载wkhtmltopdf.exe后放到本目录") 15 | exit() 16 | 17 | # PDF参数 18 | PRARMETER_PDF = '--page-size "B5" --margin-top "0.25in" --margin-right "0.25in" --margin-bottom "0.25in" --margin-left "0.3in" --encoding "UTF-8" --no-outline --footer-center "·[page]·"' 19 | 20 | 21 | class FillModel: 22 | def __init__(self, name, subject_name, exam_name, rank, errorbook): 23 | self.name = name 24 | self.subjectName = subject_name 25 | self.examName = exam_name 26 | self.rank = rank 27 | self.errorbooks = errorbook 28 | 29 | 30 | def fill_template(model): 31 | env = jinja2.Environment(loader=jinja2.FileSystemLoader("./")) 32 | tep = env.get_template("errorbook_templates.html") 33 | return tep.render(model=model) 34 | 35 | 36 | def html_to_pdf(file_name, a_subject): 37 | name = f"{a_subject.name}-{datetime.datetime.now().strftime('%Y-%m-%d')}-错题本.pdf" 38 | command = f'{WKHTMLTOPDF_PATH} {PRARMETER_PDF} {file_name} {name}' 39 | subprocess.run(command, shell=True) 40 | 41 | 42 | def clean(b_subjects): 43 | for each_subject in b_subjects: 44 | if os.path.exists(f"{each_subject.name}.html"): 45 | os.remove(f"{each_subject.name}.html") 46 | 47 | 48 | if __name__ == "__main__": 49 | print('尝试登陆中······') 50 | if os.path.exists("user.data"): 51 | zxw = load_account() 52 | else: 53 | username = input('用户名: ').strip() 54 | password = input('密码: ').strip() 55 | zxw = login_student(username, password) 56 | print("自动将账号密码加密保存在当前目录下") 57 | zxw.save_account() 58 | print('登陆成功') 59 | 60 | exams = zxw.get_page_exam(1)[0] 61 | print('考试名称:') 62 | for i, exam in enumerate(exams): 63 | print(f"{i}. {exam.name}") 64 | exam_num = int(input('请输入您要生成错题本的考试: ').strip()) 65 | cur_exam = exams[exam_num] 66 | 67 | subjects = zxw.get_subjects(cur_exam.id) 68 | 69 | mark = zxw.get_self_mark(cur_exam, has_total_score=False) 70 | for subject in subjects: 71 | cur_subject_rank = mark.find(lambda t: t.subject.code == subject.code).class_rank 72 | try: 73 | error_book = zxw.get_errorbook(cur_exam.id, subject.id) 74 | except Exception: 75 | continue 76 | errorbookHtml = fill_template(FillModel(zxw.name, subject.name, cur_exam.name, cur_subject_rank, error_book)) 77 | with open(f"{subject.name}.html", 'w', encoding='utf-8') as f: 78 | f.write(errorbookHtml) 79 | html_to_pdf(f"{subject.name}.html", subject) 80 | clean(subjects) 81 | -------------------------------------------------------------------------------- /example/student/get_mark.py: -------------------------------------------------------------------------------- 1 | # 获取成绩 2 | from zhixuewang import login_student 3 | import os 4 | 5 | if __name__ == "__main__": 6 | username = input("请输入用户名:") 7 | password = input("请输入密码:") 8 | zxw = login_student(username, password) 9 | os.system("cls") 10 | print("登录成功. 正在抓取考试列表...") 11 | exams = zxw.get_exams() 12 | if len(exams) == 0: 13 | print("你还没有考试呢~") 14 | continued = True 15 | while continued: 16 | print("考试列表:") 17 | for i, exam in enumerate(exams): 18 | print(f"{i}. {exam.name}") 19 | while True: 20 | i = input("请输入编号:").strip() 21 | if (not i.isdigit()) or int(i) < 0 or int(i) > len(exams) - 1: 22 | print("输入有误, 请重新输入") 23 | else: 24 | break 25 | print("正在查询成绩...") 26 | exam = exams[int(i)] 27 | print(zxw.get_self_mark(exam)) 28 | while True: 29 | b = input("是否再次查询(Y为是,N为不是)").strip() 30 | if b != "Y" and b != "N": 31 | print("输入有误") 32 | continue 33 | if b == "N": 34 | continued = False 35 | break 36 | -------------------------------------------------------------------------------- /example/student/get_weight_mark.py: -------------------------------------------------------------------------------- 1 | from zhixuewang import rewrite_str, login_student 2 | from zhixuewang.models import Mark 3 | 4 | 5 | @rewrite_str(Mark) 6 | def _(self): 7 | score = 0 8 | for subject in self: 9 | if subject.subject.name == "语文": 10 | score += subject.score * 0.8 11 | elif subject.subject.name == "数学": 12 | score += subject.score * 0.7 13 | elif subject.subject.name == "英语": 14 | score += subject.score * 0.5 15 | return f"加权后的分数为: {score}" # 权重: 语文 0.8; 数学 0.7; 英语 0.5; 其他科 0 16 | 17 | 18 | if __name__ == "__main__": 19 | username = input("请输入用户名:").strip() 20 | password = input("请输入密码:").strip() 21 | zxw = login_student(username, password) 22 | print(zxw.get_self_mark()) 23 | -------------------------------------------------------------------------------- /example/student/获取成绩gui/get_scores.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import sys 3 | from PySide6.QtWidgets import ( 4 | QApplication, 5 | QMessageBox, 6 | QCompleter, 7 | QTableWidgetItem, 8 | QMainWindow, 9 | ) 10 | from openpyxl import Workbook 11 | from zhixuewang import login as login_zhixuewang 12 | from zhixuewang.models import Exam, ExtendedList 13 | from PySide6.QtCore import Slot, QObject, Signal 14 | from PySide6.QtGui import QTextCursor 15 | from ui_login import Ui_Dialog 16 | from ui_mainWindow import Ui_MainWindow 17 | import pandas as pd 18 | from dataclasses import dataclass 19 | import os 20 | import traceback 21 | 22 | cur_dir = os.path.dirname(__file__) # os.path.abspath 23 | 24 | 25 | def get_path(relative_path: str): 26 | return os.path.abspath(os.path.join(cur_dir, relative_path)) 27 | 28 | 29 | USER_FILE = get_path("./user") 30 | TOKEN_FILE = get_path("token.txt") # 打包时需要修改为../token.txt 31 | with open(TOKEN_FILE) as f: 32 | token = f.readlines()[0].strip() # 可修改为你自己的token 33 | 34 | 35 | def export_to_xlsx(data, name, subject_codes): 36 | wb = Workbook() 37 | sheet = wb.active 38 | column_names = [ 39 | "姓名", 40 | "用户id", 41 | "用户code(准考证号)", 42 | "班级", 43 | "班级id", 44 | "学校", 45 | "学校id", 46 | "总分", 47 | "总分班排", 48 | "总分年排", 49 | "总分总排", 50 | ] 51 | subjects = sorted(data[0]["eachSubjectScore"], key=lambda x: x["subjectCode"]) 52 | sheet.append( 53 | column_names 54 | + [ 55 | i 56 | for each in subjects 57 | for i in [ 58 | each["subjectName"], 59 | each["subjectName"] + "班排", 60 | each["subjectName"] + "年排", 61 | each["subjectName"] + "总排", 62 | ] 63 | ] 64 | ) 65 | for each in data: 66 | added_data = [ 67 | each["userName"], 68 | each["userId"], 69 | each["userCode"], 70 | each["className"], 71 | each["classId"], 72 | each["schoolName"], 73 | each["schoolId"], 74 | each["totalScore"], 75 | each["classRank"], 76 | each["schoolRank"], 77 | each["allRank"], 78 | ] 79 | for subject_code in subject_codes: 80 | added_data.extend( 81 | [ 82 | each[f"subject{subject_code}score"], 83 | each[f"subject{subject_code}classRank"], 84 | each[f"subject{subject_code}schoolRank"], 85 | each[f"subject{subject_code}allRank"], 86 | ] 87 | ) 88 | sheet.append(added_data) 89 | wb.save(name) 90 | 91 | 92 | @dataclass 93 | class Account: 94 | username: str 95 | password: str 96 | 97 | 98 | def get_page_exams(zxw, page): 99 | r = zxw._session.get( 100 | f"https://www.zhixue.com/zxbReport/report/getPageAllExamList?pageIndex={page}&actualPosition=0&pageSize=10&reportType=exam&token=" 101 | + zxw.get_auth_header()["XToken"] 102 | ) 103 | result = r.json()["result"] 104 | if not result: 105 | return [] 106 | exams = [] 107 | for each in result["examInfoList"]: 108 | exams.append( 109 | Exam( 110 | id=each["examId"], 111 | name=each["examName"], 112 | create_time=each["examCreateDateTime"], 113 | ) 114 | ) 115 | return ExtendedList(exams) 116 | 117 | 118 | def get_exams(zxw): 119 | exams = ExtendedList() 120 | for page in range(1, 5 + 1): 121 | exams.extend(get_page_exams(zxw, page)) 122 | return exams 123 | 124 | 125 | class AccountHelper: 126 | def __init__(self) -> None: 127 | self.accounts = [] 128 | 129 | def read_account(self): 130 | self.accounts = [] 131 | if os.path.exists(USER_FILE): 132 | with open(USER_FILE, encoding="utf8") as f: 133 | for line in f.readlines(): 134 | if len(line) == 0 or "," not in line: 135 | return 136 | username, password = line.strip().split(",") 137 | self.accounts.append(Account(username, password)) 138 | 139 | def write_account(self): 140 | with open(USER_FILE, "w", encoding="utf8") as f: 141 | f.writelines( 142 | [ 143 | f"{account.username},{account.password}\n" 144 | for account in self.accounts 145 | ] 146 | ) 147 | 148 | def has_account(self, username): 149 | for account in self.accounts: 150 | if account.username == username: 151 | return True 152 | return False 153 | 154 | 155 | class Login(QMainWindow): 156 | accountHelper: AccountHelper 157 | accountMap = {} 158 | 159 | def __init__(self): 160 | super().__init__() 161 | self.ui = Ui_Dialog() 162 | # 初始化界面 163 | self.ui.setupUi(self) 164 | # 自动补全账号 165 | self.accountHelper = AccountHelper() 166 | self.accountHelper.read_account() 167 | for account in self.accountHelper.accounts: 168 | self.accountMap[account.username] = account.password 169 | completer = QCompleter(self.accountMap.keys()) 170 | self.ui.userEdit.setCompleter(completer) 171 | self.ui.userEdit.textChanged.connect(self.auto_input_pass) 172 | self.ui.button.clicked.connect(self.login_button) 173 | 174 | def auto_input_pass(self, text: str): 175 | if text in self.accountMap: 176 | self.ui.passEdit.setText(self.accountMap[text]) 177 | else: 178 | self.ui.passEdit.setText("") 179 | 180 | def login_button(self): 181 | self.ui.button.setText("登录中...") 182 | username = self.ui.userEdit.text().strip() 183 | password = self.ui.passEdit.text().strip() 184 | QApplication.processEvents() 185 | try: 186 | zxw = login_zhixuewang(username, password) 187 | if username not in self.accountMap: 188 | self.accountHelper.accounts.append( 189 | Account(username=username, password=password) 190 | ) 191 | self.accountHelper.write_account() 192 | self.mainWindow = MainWindow(zxw) 193 | self.mainWindow.show() 194 | self.close() 195 | except Exception as e: 196 | QMessageBox.about(self, "登录错误", e.value) 197 | self.ui.button.setText("登录") 198 | self.ui.passEdit.clear() 199 | 200 | 201 | @Slot(str) 202 | class Stream(QObject): 203 | newText = Signal(str) 204 | 205 | def write(self, text): 206 | self.newText.emit(str(text)) 207 | QApplication.processEvents() 208 | 209 | 210 | def find(l: list, find_f, return_f, default): 211 | for i in l: 212 | if find_f(i): 213 | return return_f(i) 214 | return default 215 | 216 | 217 | class MainWindow(QMainWindow): 218 | def __init__(self, zxw): 219 | super().__init__() 220 | self.ui = Ui_MainWindow() 221 | self.ui.setupUi(self) 222 | self.setWindowTitle("智学网查成绩 v2.2.1 (by anwenhu) 特别感谢: YS") 223 | 224 | self.zxw = zxw 225 | 226 | self.list_exams() 227 | self.ui.searchButton.clicked.connect(self.download_score) 228 | 229 | sys.stdout = Stream(newText=self.onUpdateText) # 将print重定向到框里 230 | 231 | def onUpdateText(self, text): 232 | cursor = self.ui.textEdit.textCursor() 233 | cursor.movePosition(QTextCursor.End) 234 | cursor.insertText(text) 235 | self.ui.textEdit.setTextCursor(cursor) 236 | self.ui.textEdit.ensureCursorVisible() 237 | 238 | def add_text(self, table, row, column, text): 239 | item = QTableWidgetItem() 240 | item.setText(text) 241 | table.setItem(row, column, item) 242 | 243 | def list_exams(self): 244 | table = self.ui.examTable 245 | self.exams = get_exams(self.zxw) 246 | table.setRowCount(len(self.exams)) 247 | for i, exam in enumerate(self.exams): 248 | time = datetime.datetime.strftime( 249 | datetime.datetime.fromtimestamp(exam.create_time / 1000), "%Y-%m-%d" 250 | ) 251 | self.add_text(table, i, 0, exam.id) 252 | self.add_text(table, i, 1, exam.name) 253 | self.add_text(table, i, 2, time) 254 | 255 | def download_score(self): 256 | items = self.ui.examTable.selectedItems() 257 | if len(items) == 0: 258 | self.ui.textEdit.setText("请选择考试") 259 | else: 260 | exam_id = self.ui.examTable.item(items[0].row(), 0).text() 261 | selected_exam = self.exams.find(lambda t: t.id == exam_id) 262 | print(f"正在下载{selected_exam.name}成绩... 操作会比较漫长\n") 263 | r = self.zxw._session.get( 264 | f"https://zhixuewfunction-serverllication-jpqeobvrff.cn-beijing.fcapp.run/getExamScore?examId={selected_exam.id}&token={token}" 265 | ) 266 | if "token" in r.text: 267 | print("下载失败 请检查token是否正确...\n") 268 | return 269 | try: 270 | data = r.json() 271 | 272 | s = pd.DataFrame.from_dict(data["result"]) 273 | s["totalScore"] = s.eachSubjectScore.apply( 274 | lambda t: sum([each["score"] for each in t]) 275 | ) 276 | s["allRank"] = s.totalScore.rank(method="min", ascending=False) 277 | s.classRank = ( 278 | s.groupby("classId") 279 | .totalScore.rank(method="min", ascending=False) 280 | .astype(int) 281 | ) 282 | s.schoolRank = ( 283 | s.groupby("schoolId") 284 | .totalScore.rank(method="min", ascending=False) 285 | .astype(int) 286 | ) 287 | 288 | all_subject_code = [ 289 | set(map(lambda x: x["subjectCode"], each["eachSubjectScore"])) 290 | for _, each in s.iterrows() 291 | ] 292 | subject_codes = sorted( 293 | set(all_subject_code[0]).union(*all_subject_code[1:]) 294 | ) 295 | for subject_code in subject_codes: 296 | s[f"subject{subject_code}score"] = s["eachSubjectScore"].apply( 297 | lambda t: find( 298 | t, 299 | lambda each: each["subjectCode"] == subject_code, 300 | lambda t: t["score"], 301 | 0, 302 | ) 303 | ) 304 | s[f"subject{subject_code}allRank"] = s[ 305 | f"subject{subject_code}score" 306 | ].rank(method="min", ascending=False) 307 | s[f"subject{subject_code}classRank"] = ( 308 | s.groupby("classId")[f"subject{subject_code}score"] 309 | .rank(method="min", ascending=False) 310 | .astype(int) 311 | ) 312 | s[f"subject{subject_code}schoolRank"] = ( 313 | s.groupby("schoolId")[f"subject{subject_code}score"] 314 | .rank(method="min", ascending=False) 315 | .astype(int) 316 | ) 317 | data = s.to_dict(orient="records") 318 | export_to_xlsx(data, f"考试成绩-{selected_exam.name}.xlsx", subject_codes) 319 | print("下载成功! 成绩已保存到程序所在目录下, 正在自动打开...") 320 | os.system(f"考试成绩-{selected_exam.name}.xlsx") 321 | except Exception: 322 | print("错误:\n") 323 | print(r.text) 324 | print(traceback.format_exc()) 325 | 326 | 327 | app = QApplication([]) 328 | login = Login() 329 | login.show() 330 | 331 | app.exec() 332 | -------------------------------------------------------------------------------- /example/student/获取成绩gui/ui_login.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file '.\login.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.15.4 6 | # 7 | # WARNING: Any manual changes made to this file will be lost when pyuic5 is 8 | # run again. Do not edit this file unless you know what you are doing. 9 | 10 | 11 | from PySide6 import QtCore, QtGui, QtWidgets 12 | 13 | 14 | class Ui_Dialog(object): 15 | def setupUi(self, Dialog): 16 | Dialog.setObjectName("Dialog") 17 | Dialog.resize(354, 153) 18 | Dialog.setAutoFillBackground(True) 19 | self.formLayoutWidget = QtWidgets.QWidget(Dialog) 20 | self.formLayoutWidget.setGeometry(QtCore.QRect(20, 20, 311, 81)) 21 | self.formLayoutWidget.setObjectName("formLayoutWidget") 22 | self.formLayout = QtWidgets.QFormLayout(self.formLayoutWidget) 23 | self.formLayout.setContentsMargins(0, 0, 0, 0) 24 | self.formLayout.setObjectName("formLayout") 25 | self.label = QtWidgets.QLabel(self.formLayoutWidget) 26 | font = QtGui.QFont() 27 | font.setFamily("幼圆") 28 | font.setPointSize(14) 29 | self.label.setFont(font) 30 | self.label.setObjectName("label") 31 | self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label) 32 | self.userEdit = QtWidgets.QLineEdit(self.formLayoutWidget) 33 | self.userEdit.setMinimumSize(QtCore.QSize(0, 35)) 34 | font = QtGui.QFont() 35 | font.setFamily("07YasashisaGothicBold") 36 | font.setPointSize(10) 37 | self.userEdit.setFont(font) 38 | self.userEdit.setObjectName("userEdit") 39 | self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.userEdit) 40 | self.label_2 = QtWidgets.QLabel(self.formLayoutWidget) 41 | font = QtGui.QFont() 42 | font.setFamily("幼圆") 43 | font.setPointSize(14) 44 | self.label_2.setFont(font) 45 | self.label_2.setObjectName("label_2") 46 | self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.label_2) 47 | self.passEdit = QtWidgets.QLineEdit(self.formLayoutWidget) 48 | self.passEdit.setMinimumSize(QtCore.QSize(0, 35)) 49 | font = QtGui.QFont() 50 | font.setFamily("07YasashisaGothicBold") 51 | font.setPointSize(10) 52 | self.passEdit.setFont(font) 53 | self.passEdit.setEchoMode(QtWidgets.QLineEdit.Password) 54 | self.passEdit.setObjectName("passEdit") 55 | self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.passEdit) 56 | self.button = QtWidgets.QPushButton(Dialog) 57 | self.button.setGeometry(QtCore.QRect(60, 110, 218, 36)) 58 | font = QtGui.QFont() 59 | font.setFamily("黑体") 60 | font.setPointSize(16) 61 | self.button.setFont(font) 62 | self.button.setObjectName("button") 63 | 64 | self.retranslateUi(Dialog) 65 | QtCore.QMetaObject.connectSlotsByName(Dialog) 66 | 67 | def retranslateUi(self, Dialog): 68 | _translate = QtCore.QCoreApplication.translate 69 | Dialog.setWindowTitle(_translate("Dialog", "Dialog")) 70 | self.label.setText(_translate("Dialog", "用户名:")) 71 | self.label_2.setText(_translate("Dialog", "密码:")) 72 | self.button.setText(_translate("Dialog", "登录")) -------------------------------------------------------------------------------- /example/student/获取成绩gui/ui_mainWindow.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'mainWindow.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.15.4 6 | # 7 | # WARNING: Any manual changes made to this file will be lost when pyuic5 is 8 | # run again. Do not edit this file unless you know what you are doing. 9 | 10 | 11 | from PySide6 import QtCore, QtGui, QtWidgets 12 | 13 | 14 | class Ui_MainWindow(object): 15 | def setupUi(self, MainWindow): 16 | MainWindow.setObjectName("MainWindow") 17 | MainWindow.resize(862, 736) 18 | self.centralwidget = QtWidgets.QWidget(MainWindow) 19 | self.centralwidget.setObjectName("centralwidget") 20 | self.verticalLayoutWidget = QtWidgets.QWidget(self.centralwidget) 21 | self.verticalLayoutWidget.setGeometry(QtCore.QRect(0, 10, 841, 291)) 22 | self.verticalLayoutWidget.setObjectName("verticalLayoutWidget") 23 | self.verticalLayout = QtWidgets.QVBoxLayout(self.verticalLayoutWidget) 24 | self.verticalLayout.setContentsMargins(0, 0, 0, 0) 25 | self.verticalLayout.setObjectName("verticalLayout") 26 | self.label = QtWidgets.QLabel(self.verticalLayoutWidget) 27 | font = QtGui.QFont() 28 | font.setFamily("华文中宋") 29 | font.setPointSize(14) 30 | self.label.setFont(font) 31 | self.label.setLayoutDirection(QtCore.Qt.LeftToRight) 32 | self.label.setAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignTop) 33 | self.label.setObjectName("label") 34 | self.verticalLayout.addWidget(self.label) 35 | self.examTable = QtWidgets.QTableWidget(self.verticalLayoutWidget) 36 | self.examTable.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) 37 | self.examTable.setObjectName("examTable") 38 | self.examTable.setColumnCount(3) 39 | self.examTable.setRowCount(0) 40 | item = QtWidgets.QTableWidgetItem() 41 | self.examTable.setHorizontalHeaderItem(0, item) 42 | item = QtWidgets.QTableWidgetItem() 43 | self.examTable.setHorizontalHeaderItem(1, item) 44 | item = QtWidgets.QTableWidgetItem() 45 | self.examTable.setHorizontalHeaderItem(2, item) 46 | self.verticalLayout.addWidget(self.examTable) 47 | self.searchButton = QtWidgets.QPushButton(self.centralwidget) 48 | self.searchButton.setGeometry(QtCore.QRect(300, 310, 261, 41)) 49 | font = QtGui.QFont() 50 | font.setFamily("黑体") 51 | font.setPointSize(14) 52 | self.searchButton.setFont(font) 53 | self.searchButton.setObjectName("searchButton") 54 | self.textEdit = QtWidgets.QTextEdit(self.centralwidget) 55 | self.textEdit.setGeometry(QtCore.QRect(20, 360, 811, 311)) 56 | font = QtGui.QFont() 57 | font.setFamily("隶书") 58 | font.setPointSize(18) 59 | self.textEdit.setFont(font) 60 | self.textEdit.setObjectName("textEdit") 61 | MainWindow.setCentralWidget(self.centralwidget) 62 | self.menubar = QtWidgets.QMenuBar(MainWindow) 63 | self.menubar.setGeometry(QtCore.QRect(0, 0, 862, 26)) 64 | self.menubar.setObjectName("menubar") 65 | MainWindow.setMenuBar(self.menubar) 66 | self.statusbar = QtWidgets.QStatusBar(MainWindow) 67 | self.statusbar.setObjectName("statusbar") 68 | MainWindow.setStatusBar(self.statusbar) 69 | 70 | self.retranslateUi(MainWindow) 71 | QtCore.QMetaObject.connectSlotsByName(MainWindow) 72 | 73 | def retranslateUi(self, MainWindow): 74 | _translate = QtCore.QCoreApplication.translate 75 | MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) 76 | self.label.setText(_translate("MainWindow", "考试列表")) 77 | item = self.examTable.horizontalHeaderItem(0) 78 | item.setText(_translate("MainWindow", "Id")) 79 | item = self.examTable.horizontalHeaderItem(1) 80 | item.setText(_translate("MainWindow", "名称")) 81 | item = self.examTable.horizontalHeaderItem(2) 82 | item.setText(_translate("MainWindow", "时间")) 83 | self.searchButton.setText(_translate("MainWindow", "下载成绩")) 84 | -------------------------------------------------------------------------------- /example/teacher/get_paper.py: -------------------------------------------------------------------------------- 1 | from zhixuewang import login_student, login_teacher 2 | import os 3 | 4 | if __name__ == "__main__": 5 | uname = input("请输入一个教师账号:").strip() 6 | upwd = input("请输入一个教师密码:").strip() 7 | stuname = input("请输入要查询的学生账号:").strip() 8 | stupwd = input("请输入要查询的学生密码:").strip() 9 | 10 | teacher = login_teacher(uname, upwd) 11 | student = login_student(stuname, stupwd) 12 | while True: 13 | save = input("保存到哪个文件: ") 14 | print("考试名:" + student.get_page_exam(1)[0][0].name) 15 | subjects = student.get_subjects() 16 | for i in range(0, len(subjects)): 17 | print(f"顺序ID={i} 学科={subjects[i].name}") 18 | 19 | print("请输入想查看的学科顺序id") 20 | subj_id = input() 21 | result = teacher.get_original_paper(student.id, subjects[int(subj_id)].id, save) # 获得返回值 22 | if not result: 23 | print("发生了错误,无法获得原卷。(可能是尚未生成或没有权限。)") 24 | else: 25 | os.startfile(save) 26 | print("是否继续?(y/n)") 27 | need_continue = input() 28 | if need_continue == "n" or need_continue == "N": 29 | exit(0) 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | version = "1.3.3" 4 | setup( 5 | name="zhixuewang", 6 | version=version, 7 | keywords=["智学网", "zhixue", "zhixuewang"], 8 | description="智学网的api", 9 | license="MIT", 10 | 11 | author="anwenhu,MasterYuan418,immoses648,krn1pnc,Haorwen,amakerlife", 12 | author_email="anemailpocket@163.com", 13 | 14 | packages=find_packages(), 15 | include_package_data=True, 16 | platforms="any", 17 | install_requires=["requests", "playwright"] 18 | ) 19 | -------------------------------------------------------------------------------- /zhixuewang/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = "anwenhu,MasterYuan418,immoses648,krn1pnc,Haorwen,amakerlife" 2 | __date__ = "2025/3/29 19:05" 3 | __version__ = "1.3.3" 4 | 5 | from zhixuewang.account import (login, login_id, rewrite_str, login_student, login_teacher, 6 | login_student_id, login_teacher_id, load_account) 7 | from zhixuewang.session import get_session, get_session_id 8 | 9 | VERSION = tuple(map(int, __version__.split('.'))) 10 | __all__ = [ 11 | "login", 12 | "login_id", 13 | "rewrite_str", 14 | "login_student", 15 | "login_teacher", 16 | "login_student_id", 17 | "login_teacher_id", 18 | "get_session", 19 | "get_session_id", 20 | "load_account", 21 | ] 22 | -------------------------------------------------------------------------------- /zhixuewang/account.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import pickle 3 | 4 | from zhixuewang.exceptions import RoleError 5 | from zhixuewang.models import Account, AccountData, Role 6 | from zhixuewang.session import check_is_student, get_session, get_session_id, get_basic_session 7 | from zhixuewang.student.student import StudentAccount 8 | from zhixuewang.teacher.teacher import TeacherAccount 9 | import asyncio 10 | from playwright.async_api import async_playwright, Playwright 11 | 12 | 13 | def load_account(path: str = "user.data") -> Account: 14 | with open(path, "rb") as f: 15 | data = base64.b64decode(f.read()) 16 | account_data: AccountData = pickle.loads(data) 17 | session = get_session(account_data.username, account_data.encoded_password) 18 | if account_data.role == Role.student: 19 | return StudentAccount(session).set_base_info() 20 | elif account_data.role == Role.teacher: 21 | return TeacherAccount(session).set_base_info() 22 | else: 23 | raise RoleError() 24 | 25 | 26 | def login_student_id(user_id: str, password: str) -> StudentAccount: 27 | """通过用户id和密码登录学生账号 28 | 29 | Args: 30 | user_id (str): 用户id 31 | password (str): 密码(包括加密后的密码) 32 | 33 | Raises: 34 | UserOrPassError: 用户名或密码错误 35 | UserNotFoundError: 未找到用户 36 | LoginError: 登录错误 37 | 38 | Returns: 39 | StudentAccount 40 | """ 41 | session = get_session_id(user_id, password) 42 | student = StudentAccount(session) 43 | return student.set_base_info() 44 | 45 | def login_cookie(cookies: dict) -> Account: 46 | """通过cookie登录账号 47 | 48 | Args: 49 | cookie (dict): 用户cookie 50 | 51 | Returns: 52 | Person 53 | """ 54 | session = get_basic_session() 55 | 56 | # 更新会话的cookie 57 | session.cookies.update(cookies) 58 | session.cookies.set("uname", base64.b64encode(cookies["loginUserName"].encode()).decode()) 59 | 60 | if check_is_student(session): 61 | return StudentAccount(session).set_base_info() 62 | return TeacherAccount(session).set_base_info().set_advanced_info() 63 | 64 | async def playwright_get_cookie(playwright: Playwright, username, password): 65 | chromium = playwright.chromium 66 | browser = await chromium.launch(headless=False) 67 | context = await browser.new_context() 68 | page = await context.new_page() 69 | await page.goto("https://www.zhixue.com/wap_login.html") 70 | await page.wait_for_load_state('networkidle') # 等待网络状态为空闲 71 | print(await page.title()) 72 | await page.fill('#txtUserName', username) 73 | await page.fill('#txtPassword', password) 74 | 75 | # 点击注册按钮 76 | await asyncio.sleep(0.5) 77 | await page.click('#signup_button') 78 | await page.wait_for_url("https://www.zhixue.com/htm-vessel/**", timeout=float('inf')) 79 | cookies = await page.context.cookies() 80 | # print("Cookies:", cookies) 81 | # 将Cookie转换为字典 82 | cookies_dict = {cookie['name']: cookie['value'] for cookie in cookies} 83 | await browser.close() 84 | return cookies_dict 85 | 86 | async def playwright_process(username: str, password: str): 87 | async with async_playwright() as playwright: 88 | return await playwright_get_cookie(playwright, username, password) 89 | 90 | def login_playwright(username: str, password: str) -> Account: 91 | """通过playwright更加便利的登录账号 92 | 93 | Args: 94 | username (str): 用户名, 可以为准考证号, 手机号 95 | password (str): 密码 96 | 97 | Returns: 98 | Person 99 | """ 100 | session = get_basic_session() 101 | 102 | # 更新会话的cookie 103 | cookies = asyncio.run(playwright_process(username, password)) 104 | session.cookies.update(cookies) 105 | session.cookies.set("uname", base64.b64encode(cookies["loginUserName"].encode()).decode()) 106 | 107 | if check_is_student(session): 108 | return StudentAccount(session).set_base_info() 109 | return TeacherAccount(session).set_base_info().set_advanced_info() 110 | 111 | 112 | def login_student(username: str, password: str) -> StudentAccount: 113 | """通过用户名和密码登录学生账号 114 | 115 | Args: 116 | username (str): 用户名, 可以为准考证号, 手机号 117 | password (str): 密码(包括加密后的密码) 118 | 119 | Raises: 120 | UserOrPassError: 用户名或密码错误 121 | UserNotFoundError: 未找到用户 122 | LoginError: 登录错误 123 | 124 | Returns: 125 | StudentAccount 126 | """ 127 | session = get_session(username, password) 128 | student = StudentAccount(session) 129 | return student.set_base_info() 130 | 131 | 132 | def login_teacher_id(user_id: str, password: str) -> TeacherAccount: 133 | """通过用户id和密码登录老师账号 134 | 135 | Args: 136 | user_id (str): 用户id 137 | password (str): 密码(包括加密后的密码) 138 | 139 | Raises: 140 | UserOrPassError: 用户名或密码错误 141 | UserNotFoundError: 未找到用户 142 | LoginError: 登录错误 143 | 144 | Returns: 145 | TeacherAccount 146 | """ 147 | session = get_session_id(user_id, password) 148 | teacher = TeacherAccount(session) 149 | return teacher.set_base_info().set_advanced_info() 150 | 151 | 152 | def login_teacher(username: str, password: str) -> TeacherAccount: 153 | """通过用户名和密码登录老师账号 154 | 155 | Args: 156 | username (str): 用户名, 可以为准考证号, 手机号 157 | password (str): 密码(包括加密后的密码) 158 | 159 | Raises: 160 | UserOrPassError: 用户名或密码错误 161 | UserNotFoundError: 未找到用户 162 | LoginError: 登录错误 163 | 164 | Returns: 165 | TeacherAccount 166 | """ 167 | session = get_session(username, password) 168 | teacher = TeacherAccount(session) 169 | return teacher.set_base_info().set_advanced_info() 170 | 171 | 172 | def login_id(user_id: str, password: str) -> Account: 173 | """通过用户id和密码登录智学网 174 | 175 | Args: 176 | user_id (str): 用户id 177 | password (str): 密码(包括加密后的密码) 178 | 179 | Raises: 180 | UserOrPassError: 用户名或密码错误 181 | UserNotFoundError: 未找到用户 182 | LoginError: 登录错误 183 | RoleError: 账号角色未知 184 | 185 | Returns: 186 | Person 187 | """ 188 | session = get_session_id(user_id, password) 189 | if check_is_student(session): 190 | return StudentAccount(session).set_base_info() 191 | return TeacherAccount(session).set_base_info() 192 | 193 | 194 | def login(username: str, password: str) -> Account: 195 | """通过用户名和密码登录智学网 196 | 197 | Args: 198 | username (str): 用户名, 可以为准考证号, 手机号 199 | password (str): 密码(包括加密后的密码) 200 | 201 | Raises: 202 | ArgError: 参数错误 203 | UserOrPassError: 用户名或密码错误 204 | UserNotFoundError: 未找到用户 205 | LoginError: 登录错误 206 | RoleError: 账号角色未知 207 | 208 | Returns: 209 | Person 210 | """ 211 | session = get_session(username, password) 212 | if check_is_student(session): 213 | return StudentAccount(session).set_base_info() 214 | return TeacherAccount(session).set_base_info().set_advanced_info() 215 | 216 | 217 | def rewrite_str(model): 218 | """重写类的__str__方法 219 | 220 | Args: 221 | model: 需重写__str__方法的类 222 | 223 | Examples: 224 | >>> from zhixuewang.models import School 225 | >>> @rewrite_str(School) 226 | >>> def _(self: School): 227 | >>> return f"" 228 | >>> print(School("test id", "test school")) 229 | 230 | """ 231 | 232 | def str_decorator(func): 233 | model.__str__ = func 234 | return func 235 | 236 | return str_decorator 237 | -------------------------------------------------------------------------------- /zhixuewang/exceptions.py: -------------------------------------------------------------------------------- 1 | class Error(Exception): 2 | value: str 3 | 4 | def __str__(self): 5 | return str(self.value) 6 | 7 | 8 | class LoginError(Error): 9 | def __init__(self, value): 10 | self.value = value 11 | 12 | 13 | class UserOrPassError(LoginError): 14 | def __init__(self, value=None): 15 | super().__init__(value or "用户名或密码错误!") 16 | 17 | 18 | class UserNotFoundError(LoginError): 19 | def __init__(self, value=None): 20 | super().__init__(value or "用户不存在!") 21 | 22 | 23 | class UserDefunctError(LoginError): 24 | def __init__(self, value=None): 25 | super().__init__(value or "用户已失效!") 26 | 27 | 28 | class RoleError(Error): 29 | def __init__(self, value=None): 30 | self.value = value or "账号是未知用户" 31 | 32 | 33 | class ArgError(Error): 34 | def __init__(self, value=None): 35 | self.value = value or "请输入正确的参数!" 36 | 37 | 38 | class PageConnectionError(Error): 39 | def __init__(self, value): 40 | self.value = value 41 | -------------------------------------------------------------------------------- /zhixuewang/models.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from enum import Enum 3 | import os 4 | import pickle 5 | from typing import List, Callable, Union, TypeVar 6 | from datetime import datetime 7 | from dataclasses import dataclass, field 8 | from zhixuewang.session import get_basic_session, get_session 9 | from zhixuewang.tools.datetime_tool import get_property 10 | from zhixuewang.urls import Url 11 | 12 | 13 | class Role(Enum): 14 | student = 0 15 | teacher = 1 16 | 17 | 18 | @dataclass 19 | class AccountData: 20 | username: str 21 | encoded_password: str 22 | role: Role 23 | 24 | 25 | class Account: 26 | def __init__(self, session, role: Role) -> None: 27 | self._session = session 28 | self.role = role 29 | self.username = base64.b64decode(session.cookies["uname"].encode()).decode() 30 | 31 | def save_account(self, path: str = "user.data"): 32 | with open(path, "wb") as f: 33 | password = base64.b64decode(self._session.cookies["pwd"].encode()).decode() 34 | data = pickle.dumps( 35 | AccountData( 36 | self.username, password, self.role 37 | ) 38 | ) 39 | f.write(base64.b64encode(data)) 40 | 41 | def update_login_status(self): 42 | """更新登录状态. 如果session过期自动重新获取""" 43 | r = self._session.get(Url.GET_LOGIN_STATE) 44 | data = r.json() 45 | if data["result"] == "success": 46 | return 47 | # session过期 48 | password = base64.b64decode(self._session.cookies["pwd"].encode()).decode() 49 | self._session = get_session(self.username, password) 50 | 51 | 52 | T = TypeVar("T") 53 | 54 | 55 | class ExtendedList(List[T]): 56 | """扩展列表, 方便找到列表里的元素""" 57 | 58 | def __init__(self, ls: List[T] = None): 59 | super().__init__(list() if ls is None else ls) 60 | 61 | def foreach(self, f: Callable[[T], None]): 62 | for each in self: 63 | f(each) 64 | 65 | def find(self, f: Callable[[T], bool]) -> Union[T, None]: 66 | """返回列表里满足函数f的第一个元素""" 67 | result = (each for each in self if f(each)) 68 | try: 69 | return next(result) 70 | except StopIteration: 71 | return None 72 | 73 | def find_all(self, f: Callable[[T], bool]) -> "ExtendedList[T]": 74 | """返回列表里所有满足函数f的元素""" 75 | result = (each for each in self if f(each)) 76 | return ExtendedList(list(result)) 77 | 78 | def find_by_name(self, name: str) -> Union[T, None]: 79 | """返回列表里第一个特定名字的元素, 没有则返回None""" 80 | return self.find(lambda d: d.name == name) 81 | 82 | def find_all_by_name(self, name: str) -> "ExtendedList[T]": 83 | """返回列表里所有特定名字的元素""" 84 | return self.find_all(lambda d: d.name == name) 85 | 86 | def find_by_id(self, spec_id: str) -> Union[T, None]: 87 | """返回列表里第一个特定id的元素, 没有则返回None""" 88 | return self.find(lambda d: d.id == spec_id) 89 | 90 | def find_all_by_id(self, spec_id: str) -> "ExtendedList[T]": 91 | """返回列表里所有特定id的元素""" 92 | return self.find_all(lambda d: d.id == spec_id) 93 | 94 | 95 | @dataclass 96 | class Grade: 97 | """年级""" 98 | 99 | name: str = "" 100 | code: str = "" 101 | phase_name: str = "" 102 | phase_code: str = "" 103 | 104 | 105 | @dataclass 106 | class School: 107 | """学校""" 108 | 109 | id: str = "" 110 | name: str = "" 111 | 112 | def __str__(self): 113 | return self.name 114 | 115 | 116 | class Sex(Enum): 117 | """性别""" 118 | 119 | GIRL = "女" 120 | BOY = "男" 121 | 122 | def __str__(self): 123 | return self._value_ 124 | 125 | 126 | @dataclass(eq=False) 127 | class StuClass: 128 | """班级""" 129 | 130 | id: str = "" 131 | name: str = "" 132 | grade: Grade = field(default_factory=Grade, repr=False) 133 | school: School = field(default_factory=School, repr=False) 134 | 135 | def __eq__(self, other): 136 | return type(other) == type(self) and other.id == self.id 137 | 138 | def __str__(self): 139 | return f"学校: {self.school} 班级: {self.name}" 140 | 141 | 142 | @dataclass(repr=False) 143 | class Person: 144 | """一些基本属性""" 145 | 146 | id: str = "" 147 | name: str = "" 148 | gender: Sex = Sex.GIRL 149 | mobile: str = "" 150 | avatar: str = "" 151 | 152 | 153 | @dataclass(repr=False) 154 | class StuPerson(Person): 155 | """一些关于学生的信息""" 156 | 157 | code: str = "" 158 | clazz: StuClass = field(default_factory=StuClass, repr=False) 159 | 160 | def __str__(self): 161 | return ( 162 | f"{self.clazz} 姓名: {self.name} 性别: {self.gender} " 163 | f"{f'手机号码: {self.mobile}' if self.mobile != '' else ''}" 164 | ) 165 | 166 | 167 | @dataclass 168 | class BasicSubject: 169 | """学科基本信息""" 170 | 171 | name: str = "" 172 | code: str = "" 173 | 174 | 175 | @dataclass(eq=False) 176 | class Subject(BasicSubject): 177 | """学科""" 178 | 179 | id: str = "" 180 | standard_score: float = 0 181 | status: str = field(default="", repr=False) 182 | exam_id: str = field(default="", repr=False) 183 | create_user: Person = field(default_factory=Person, repr=False) 184 | create_time: float = field(default=0, repr=False) 185 | 186 | def __eq__(self, other): 187 | return type(other) == type(self) and other.id == self.id 188 | 189 | 190 | @dataclass 191 | class TextBook: 192 | """教科书属性""" 193 | code: str = "" 194 | """教科书编号""" 195 | name: str = "" 196 | """教科书名称""" 197 | version: str = "" 198 | """教科书版本,如北师大、人教、部编等""" 199 | versionCode: int = 0 200 | """教科书版本编号""" 201 | bindSubject: BasicSubject = field(default_factory=BasicSubject) 202 | def __str__(self) -> str: 203 | return f"{self.bindSubject.name} {self.name} ({self.version})" 204 | 205 | 206 | @dataclass(eq=False) 207 | class Exam: 208 | """考试""" 209 | 210 | id: str = "" 211 | name: str = "" 212 | status: str = "" 213 | grade_code: str = "" 214 | subjects: ExtendedList[Subject] = field(default_factory=ExtendedList, repr=False) 215 | schools: ExtendedList[School] = field(default_factory=ExtendedList, repr=False) 216 | create_user: Person = field(default_factory=Person, repr=False) 217 | create_time: float = field(default=0, repr=False) 218 | class_rank: int = field(default=0, repr=False) 219 | grade_rank: int = field(default=0, repr=False) 220 | is_final: bool = False 221 | 222 | def __bool__(self): 223 | return bool(self.id) 224 | 225 | def __eq__(self, other): 226 | return type(other) == type(self) and other.id == self.id 227 | 228 | 229 | @dataclass 230 | class SubjectScore: 231 | """一门学科的成绩""" 232 | 233 | score: float = 0 234 | subject: Subject = field(default_factory=Subject) 235 | person: StuPerson = field(default_factory=StuPerson) 236 | class_rank: int = field(default_factory=int, compare=False) 237 | grade_rank: int = field(default_factory=int, compare=False) 238 | exam_rank: int = field(default_factory=int, compare=False) 239 | 240 | def __str__(self) -> str: 241 | if self.person.id == "": # mark 242 | data = f"{self.subject.name}: {self.score}" 243 | if self.class_rank != 0: 244 | data += f" (班级第{self.class_rank}名)" 245 | return data 246 | return self.__repr__() 247 | 248 | 249 | class Mark(ExtendedList[SubjectScore]): 250 | """一场考试的成绩""" 251 | 252 | def __init__( 253 | self, ls: list = None, exam: Exam = Exam(), person: StuPerson = StuPerson() 254 | ): 255 | 256 | super().__init__([] if ls is None else ls) 257 | self.exam = exam 258 | self.person = person 259 | 260 | def __repr__(self): 261 | if self.exam and self.person: 262 | msg = f"{self.person.name}-{self.exam.name}\n" + "".join( 263 | [f"{subject}\n" for subject in self] 264 | ) 265 | return msg[:-1] 266 | 267 | def __str__(self): 268 | return self.__repr__() 269 | 270 | 271 | @dataclass 272 | class MarkingRecord: 273 | """批改记录""" 274 | 275 | time: datetime 276 | score: float 277 | teacher_name: str 278 | 279 | 280 | @dataclass 281 | class SubTopicRecord: 282 | """小题得分详情""" 283 | 284 | score: float 285 | marking_records: Union[None, ExtendedList[MarkingRecord]] 286 | 287 | 288 | @dataclass 289 | class TopicRecord: 290 | """题目得分详情""" 291 | 292 | title: str 293 | score: float 294 | standard_score: float 295 | subtopic_records: Union[None, ExtendedList[SubTopicRecord]] 296 | 297 | 298 | class AnswerRecord(ExtendedList[TopicRecord]): 299 | """一场考试的得分详情""" 300 | 301 | 302 | @dataclass 303 | class HwType: 304 | """作业类型, eg: 105 自由出题""" 305 | 306 | name: str 307 | code: int 308 | 309 | 310 | @dataclass 311 | class Homework: 312 | id: str 313 | title: str = "" 314 | type: HwType = field(default_factory=HwType) 315 | begin_time: int = 0 316 | end_time: int = 0 317 | create_time: int = 0 318 | subject_name: str = "" 319 | is_allow_makeup: bool = False # 是否允许重做 320 | class_id: str = "" 321 | 322 | 323 | @dataclass 324 | class StuHomework(Homework): 325 | stu_hwid: str = "" 326 | 327 | 328 | @dataclass 329 | class HwResource: 330 | path: str 331 | name: str 332 | 333 | def download(self, path: str): 334 | r = get_basic_session().get(self.path) 335 | with open(os.path.join(path, self.name), "wb") as f: 336 | f.write(r.content) 337 | 338 | 339 | @dataclass 340 | class HwAnswer: 341 | title: str = "" 342 | content: str = "" 343 | 344 | 345 | @dataclass 346 | class ErrorBookTopic: 347 | analysis_html: str 348 | answer_html: str 349 | answer_type: str 350 | is_correct: bool 351 | class_score_rate: float 352 | content_html: str 353 | difficulty: int 354 | dis_title_number: str 355 | paper_id: str 356 | subject_name: str 357 | score: float 358 | standard_answer: str # 网址 359 | standard_score: float 360 | topic_set_id: str 361 | topic_img_url: str # 好看的题目 362 | topic_source_paper_name: str 363 | image_answer: List[str] # 你的答案 364 | topic_analysis_img_url: str 365 | 366 | @dataclass 367 | class AcademicYear: 368 | """学年""" 369 | 370 | name: str 371 | code: str 372 | begin_time: str 373 | end_time: str 374 | -------------------------------------------------------------------------------- /zhixuewang/session.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | from zhixuewang.exceptions import LoginError, UserNotFoundError, UserOrPassError 4 | import base64 5 | from zhixuewang.urls import Url 6 | 7 | 8 | def get_basic_session() -> requests.Session: 9 | session = requests.Session() 10 | session.trust_env = False 11 | session.headers[ 12 | "User-Agent" 13 | ] = "Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1" 14 | return session 15 | 16 | 17 | def get_session(username: str, password: str, _type: str = "auto") -> requests.Session: 18 | """通过用户名和密码获取session 19 | 20 | 默认可支持zx, zxt和tch开头的账号, 准考证号以及手机号 21 | 可通过改变type为id来支持使用用户id 22 | 23 | Args: 24 | username (str): 用户名, 可以为准考证号, 手机号, id 25 | password (str): 密码(包括加密后的密码) 26 | _type (str): 登录方式, 为id时表示用id登录, 为auto时表示自动选择登录方式 27 | 28 | Raises: 29 | UserOrPassError: 用户名或密码错误 30 | UserNotFoundError: 未找到用户 31 | LoginError: 登录错误 32 | 33 | Returns: 34 | requests.session: 35 | """ 36 | if len(password) != 32: 37 | password = ( 38 | pow( 39 | int.from_bytes(password.encode()[::-1], "big"), 40 | 65537, 41 | 186198350384465244738867467156319743461, 42 | ) 43 | .to_bytes(16, "big") 44 | .hex() 45 | ) # by immoses648 46 | session = get_basic_session() 47 | r = session.get(Url.SSO_URL, proxies={'https': None, 'http': None}) 48 | 49 | json_obj = json.loads(r.text.strip().replace("\\", "").replace("'", "")[1:-1]) 50 | if json_obj["code"] != 1000: 51 | raise LoginError(json_obj["data"]) 52 | lt = json_obj["data"]["lt"] 53 | execution = json_obj["data"]["execution"] 54 | r = session.get( 55 | Url.SSO_URL, 56 | params={ 57 | "encode": "true", 58 | "sourceappname": "tkyh,tkyh", 59 | "_eventId": "submit", 60 | "appid": "zx-container-client", 61 | "client": "web", 62 | "type": "loginByNormal", 63 | "key": _type, 64 | "lt": lt, 65 | "execution": execution, 66 | "customLogoutUrl": "https://www.zhixue.com/login.html", 67 | "username": username, 68 | "password": password, 69 | }, 70 | ) 71 | json_obj = json.loads(r.text.strip().replace("\\", "").replace("'", "")[1:-1]) 72 | if json_obj["code"] != 1001: 73 | if json_obj["code"] == 1002: 74 | raise UserOrPassError() 75 | if json_obj["code"] == 2009: 76 | raise UserNotFoundError() 77 | raise LoginError(json_obj["data"]) 78 | ticket = json_obj["data"]["st"] 79 | session.post( 80 | Url.SERVICE_URL, 81 | data={ 82 | "action": "login", 83 | "ticket": ticket, 84 | }, 85 | ) 86 | session.cookies.set("uname", base64.b64encode(username.encode()).decode()) 87 | session.cookies.set("pwd", base64.b64encode(password.encode()).decode()) 88 | return session 89 | 90 | 91 | def get_session_id(user_id: str, password: str) -> requests.Session: 92 | """通过用户id和密码获取session 93 | 94 | Args: 95 | user_id (str): 用户id 96 | password (str): 密码(包括加密后的密码) 97 | 98 | Raises: 99 | UserOrPassError: 用户名或密码错误 100 | UserNotFoundError: 未找到用户 101 | LoginError: 登录错误 102 | 103 | Returns: 104 | requests.session: 105 | """ 106 | return get_session(user_id, password, "id") 107 | 108 | 109 | def get_user_id(username: str, password: str) -> str: 110 | """返回用户id 111 | 112 | Args: 113 | username (str): 用户名, 可以为准考证号, 手机号 114 | password (str): 密码 115 | 116 | Raises: 117 | UserOrPassError: 用户名或密码错误 118 | 119 | Returns: 120 | str: 用户id 121 | """ 122 | session = requests.Session() 123 | session.headers[ 124 | "User-Agent" 125 | ] = "Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1" 126 | r = session.post( 127 | Url.TEST_PASSWORD_URL, 128 | data={"loginName": username, "password": password, "code": ""}, 129 | ) 130 | json_obj = r.json() # {"data": ErrorMsg, "result": StatusCode} 131 | if json_obj.get("data"): 132 | return json_obj["data"] 133 | if json_obj["result"] != "success": 134 | raise UserOrPassError() 135 | return "" 136 | 137 | 138 | def check_is_student(s: requests.Session) -> bool: 139 | """判断用户是否为学生 140 | 141 | Args: 142 | s (requests.session): session 143 | 144 | Returns: 145 | bool: 146 | """ 147 | url = s.get("https://www.zhixue.com/container/container/index/").url 148 | return "student" in url 149 | -------------------------------------------------------------------------------- /zhixuewang/student/__init__.py: -------------------------------------------------------------------------------- 1 | from zhixuewang.student.student import StudentAccount 2 | 3 | __all__ = ["StudentAccount"] 4 | -------------------------------------------------------------------------------- /zhixuewang/student/student.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | import time 4 | import uuid 5 | from typing import List, Optional, Tuple, Union 6 | from zhixuewang.models import ( 7 | AcademicYear, 8 | Account, 9 | ErrorBookTopic, 10 | ExtendedList, 11 | Exam, 12 | HwResource, 13 | HwType, 14 | HwAnswer, 15 | Mark, 16 | MarkingRecord, 17 | SubTopicRecord, 18 | TopicRecord, 19 | AnswerRecord, 20 | Role, 21 | StuHomework, 22 | Subject, 23 | SubjectScore, 24 | StuClass, 25 | School, 26 | Sex, 27 | Grade, 28 | StuPerson 29 | ) 30 | from zhixuewang.exceptions import ( 31 | UserDefunctError, 32 | PageConnectionError, 33 | ) 34 | from zhixuewang.student.urls import Url 35 | from datetime import datetime 36 | 37 | 38 | def _check_is_uuid(msg: str): 39 | """判断msg是否为uuid""" 40 | return ( 41 | len(msg) == 36 42 | and msg[14] == "4" 43 | and msg[8] == msg[13] == msg[18] == msg[23] == "-" 44 | ) 45 | 46 | 47 | def _md5_encode(msg: str) -> str: 48 | md5 = hashlib.md5() 49 | md5.update(msg.encode(encoding="utf-8")) 50 | return md5.hexdigest() 51 | 52 | 53 | class StudentAccount(Account, StuPerson): 54 | """学生账号""" 55 | 56 | def __init__(self, session): 57 | super().__init__(session, Role.student) 58 | # self._token_timestamp = ["", 0] 59 | self._auth = { 60 | "token": "", 61 | "timestamp": 0.0 62 | } 63 | self.exams: ExtendedList[Exam] = ExtendedList() 64 | 65 | def get_session(self): 66 | '''获得学生端Session''' 67 | return self._session 68 | 69 | def get_auth_header(self) -> dict: 70 | """获取header""" 71 | self.update_login_status() 72 | auth_guid = str(uuid.uuid4()) 73 | auth_time_stamp = str(int(time.time() * 1000)) 74 | auth_token = _md5_encode(auth_guid + auth_time_stamp + "iflytek!@#123student") 75 | token, cur_time = self._auth["token"], self._auth["timestamp"] 76 | if token and time.time() - cur_time < 600: # 判断token是否过期 77 | return { 78 | "authbizcode": "0001", 79 | "authguid": auth_guid, 80 | "authtimestamp": auth_time_stamp, 81 | "authtoken": auth_token, 82 | "XToken": token, 83 | } 84 | r = self._session.get( 85 | Url.XTOKEN_URL, 86 | headers={ 87 | "authbizcode": "0001", 88 | "authguid": auth_guid, 89 | "authtimestamp": auth_time_stamp, 90 | "authtoken": auth_token, 91 | }, 92 | ) 93 | if not r.ok: 94 | raise PageConnectionError(f"get_auth_header出错 \n {r.text}") 95 | self._auth["token"] = r.json()["result"] 96 | self._auth["timestamp"] = time.time() 97 | return self.get_auth_header() 98 | 99 | def set_base_info(self): 100 | """设置账户基本信息, 如用户id, 姓名, 学校等""" 101 | self.update_login_status() 102 | r = self._session.get(Url.INFO_URL) 103 | if not r.ok: 104 | raise PageConnectionError(f"set_base_info出错 \n {r.text}") 105 | json_data = r.json()["student"] 106 | if not json_data.get("clazz", False): 107 | raise UserDefunctError() 108 | self.code = json_data.get("code") 109 | self.name = json_data.get("name") 110 | self.avatar = json_data.get("avatar") 111 | self.gender = Sex.BOY if json_data.get("gender") == "1" else Sex.GIRL 112 | self.username = json_data.get("loginName") 113 | self.id = json_data.get("id") 114 | self.mobile = json_data.get("mobile") 115 | self.clazz = StuClass( 116 | id=json_data["clazz"]["id"], 117 | name=json_data["clazz"]["name"], 118 | school=School( 119 | id=json_data["clazz"]["division"]["school"]["id"], 120 | name=json_data["clazz"]["division"]["school"]["name"], 121 | ), 122 | grade=Grade( 123 | code=json_data["clazz"]["division"]["grade"]["code"], 124 | name=json_data["clazz"]["division"]["grade"]["name"], 125 | phase_code=json_data["clazz"]["division"]["grade"]["phase"]["code"], 126 | phase_name=json_data["clazz"]["division"]["grade"]["phase"]["name"], 127 | ), 128 | ) 129 | return self 130 | 131 | def get_academic_year(self) -> ExtendedList[AcademicYear]: 132 | """获取学年 133 | 134 | Returns: 135 | ExtendedList[Tuple[str, str]]: 学年列表 136 | """ 137 | self.update_login_status() 138 | r = self._session.get( 139 | Url.GET_ACADEMIC_YEAR_URL, 140 | headers=self.get_auth_header(), 141 | ) 142 | if not r.ok: 143 | raise PageConnectionError(f"get_academic_year 出错 \n {r.text}") 144 | json_data = r.json()["result"] 145 | 146 | academic_years: ExtendedList[AcademicYear] = ExtendedList() 147 | for academic_year in json_data: 148 | academic_years.append( 149 | AcademicYear( 150 | name=academic_year["name"], 151 | code=academic_year["code"], 152 | begin_time=academic_year["beginTime"], 153 | end_time=academic_year["endTime"], 154 | ) 155 | ) 156 | return academic_years 157 | 158 | def _get_latest_valid_academic_year(self) -> AcademicYear: 159 | def check_is_valid(exam: str) -> bool: 160 | return exam != "" 161 | academic_years = self.get_academic_year() 162 | cnt = 0 163 | while True: 164 | start_school_year = academic_years[cnt].begin_time 165 | end_school_year = academic_years[cnt].end_time 166 | cnt += 1 167 | r = self._session.get( 168 | Url.GET_RECENT_EXAM_URL, 169 | params={ 170 | "startSchoolYear": start_school_year, 171 | "endSchoolYear": end_school_year, 172 | }, 173 | headers=self.get_auth_header() 174 | ) 175 | if check_is_valid(r.json()["result"]): 176 | break 177 | return academic_years[cnt - 1] 178 | 179 | def get_exam(self, exam_data: Union[Exam, str] = "") -> Exam: 180 | """获取考试 181 | 182 | Args: 183 | exam_data (Union[Exam, str]): 考试id 或 考试名称, 为Exam实例时直接返回, 为默认值时返回最新考试 184 | 185 | Returns: 186 | Exam 187 | """ 188 | if not exam_data: 189 | return self.get_latest_exam() 190 | if isinstance(exam_data, Exam): 191 | if not exam_data: 192 | return self.get_latest_exam() 193 | elif exam_data.class_rank and exam_data.grade_rank: 194 | return exam_data 195 | else: 196 | return self.get_exams().find_by_id(exam_data.id) 197 | exams = self.get_exams() 198 | if _check_is_uuid(exam_data): 199 | exam = exams.find_by_id(exam_data) # 为id 200 | else: 201 | exam = exams.find_by_name(exam_data) 202 | return exam 203 | 204 | def get_page_exam(self, page_index: int, acamemic_year: AcademicYear) -> Tuple[ExtendedList[Exam], bool]: 205 | """获取指定页数的考试列表""" 206 | self.update_login_status() 207 | exams: ExtendedList[Exam] = ExtendedList() 208 | r = self._session.get( 209 | Url.GET_EXAM_URL, 210 | params={ 211 | "pageIndex": page_index, 212 | "pageSize": 10, 213 | "startSchoolYear": acamemic_year.begin_time, 214 | "endSchoolYear": acamemic_year.end_time, 215 | }, 216 | headers=self.get_auth_header(), 217 | ) 218 | if not r.ok: 219 | raise PageConnectionError(f"get_page_exam出错 \n {r.text}") 220 | json_data = r.json()["result"] 221 | for exam_data in json_data["examList"]: 222 | exam = Exam(id=exam_data["examId"], name=exam_data["examName"]) 223 | exam.create_time = exam_data["examCreateDateTime"] 224 | exams.append(exam) 225 | has_next_page: bool = json_data["hasNextPage"] 226 | return exams, has_next_page 227 | 228 | def get_latest_exam(self) -> Exam: 229 | """获取最新考试""" 230 | 231 | self.update_login_status() 232 | academic_year = self._get_latest_valid_academic_year() 233 | start_school_year = academic_year.begin_time 234 | end_school_year = academic_year.end_time 235 | 236 | r = self._session.get( 237 | Url.GET_RECENT_EXAM_URL, 238 | params={ 239 | "startSchoolYear": start_school_year, 240 | "endSchoolYear": end_school_year, 241 | }, 242 | headers=self.get_auth_header() 243 | ) 244 | if not r.ok: 245 | raise PageConnectionError(f"get_latest_exam出错 \n {r.text}") 246 | json_data = r.json()["result"] 247 | 248 | exam_info_data = json_data["examInfo"] 249 | 250 | subjects: ExtendedList[Subject] = ExtendedList() 251 | 252 | for subject_data in exam_info_data["subjectScores"]: 253 | subjects.append( 254 | Subject( 255 | id=subject_data["topicSetId"], 256 | name=subject_data["subjectName"], 257 | code=subject_data["subjectCode"], 258 | ) 259 | ) 260 | 261 | exam = Exam( 262 | id=exam_info_data["examId"], 263 | name=exam_info_data["examName"], 264 | subjects=subjects, 265 | grade_code=json_data["gradeCode"], 266 | is_final=exam_info_data["isFinal"], 267 | ) 268 | exam.create_time = exam_info_data["examCreateDateTime"] 269 | return exam 270 | 271 | def get_exams(self) -> ExtendedList[Exam]: 272 | """获取所有考试""" 273 | 274 | # 缓存 275 | if len(self.exams) > 0: 276 | latest_exam = self.get_latest_exam() 277 | if self.exams[0].id == latest_exam.id: 278 | return self.exams 279 | 280 | academic_years = self.get_academic_year() 281 | exams: ExtendedList[Exam] = ExtendedList() 282 | for year in academic_years: 283 | i = 1 284 | check = True 285 | while check: 286 | cur_exams, check = self.get_page_exam(i, year) 287 | exams.extend(cur_exams) 288 | i += 1 289 | self.exams = exams 290 | return exams 291 | 292 | def __get_self_mark(self, exam: Exam, has_total_score: bool, academic_year: AcademicYear) -> Mark: 293 | self.update_login_status() 294 | mark = Mark(exam=exam, person=self) 295 | r = self._session.get( 296 | Url.GET_MARK_URL, 297 | params={"examId": exam.id}, 298 | headers=self.get_auth_header(), 299 | ) 300 | if not r.ok: 301 | raise PageConnectionError(f"__get_self_mark出错 \n {r.text}") 302 | json_data = r.json() 303 | json_data = json_data["result"] 304 | # exam.name = json_data["total_score"]["examName"] 305 | # exam.id = json_data["total_score"]["examId"] 306 | for subject in json_data["paperList"]: 307 | subject_score = SubjectScore( 308 | score=subject["userScore"], 309 | subject=Subject( 310 | id=subject["paperId"], 311 | name=subject["subjectName"], 312 | code=subject["subjectCode"], 313 | standard_score=subject["standardScore"], 314 | exam_id=exam.id, 315 | ), 316 | person=StuPerson(), 317 | ) 318 | mark.append(subject_score) 319 | total_score = json_data.get("totalScore") 320 | if has_total_score and total_score: 321 | subject_score = SubjectScore( 322 | score=total_score["userScore"], 323 | subject=Subject( 324 | id="", 325 | name=total_score["subjectName"], 326 | code="99", 327 | standard_score=total_score["standardScore"], 328 | exam_id=exam.id, 329 | ), 330 | person=StuPerson(), 331 | class_rank=exam.class_rank, 332 | grade_rank=exam.grade_rank, 333 | ) 334 | mark.append(subject_score) 335 | self._set_exam_rank(mark, academic_year) 336 | return mark 337 | 338 | def get_self_mark( 339 | self, exam_data: Union[Exam, str] = "", has_total_score: bool = True, academic_year: Optional[AcademicYear] = None 340 | ) -> Mark: 341 | """获取指定考试的成绩 342 | 343 | 若传考试则必传学年,也可两者都不传 344 | TODO: 支持仅传学年 345 | 346 | Args: 347 | exam_data (Union[Exam, str]): 考试id 或 考试名称 或 Exam实例, 默认值为最新考试 348 | has_total_score (bool): 是否计算总分, 默认为True 349 | academic_year (Optional[AcademicYear]): 学年, 默认为None 350 | 351 | Returns: 352 | Mark 353 | """ 354 | if exam_data and (not academic_year): 355 | raise ValueError("在指定考试的情况下, 应当指定学年") 356 | exam = self.get_exam(exam_data) 357 | if exam is None: 358 | return Mark() 359 | if not academic_year: 360 | academic_year = self._get_latest_valid_academic_year() 361 | return self.__get_self_mark(exam, has_total_score, academic_year) 362 | 363 | def __get_subjects(self, exam: Exam) -> ExtendedList[Subject]: 364 | self.update_login_status() 365 | subjects: ExtendedList[Subject] = ExtendedList() 366 | r = self._session.get( 367 | Url.GET_SUBJECT_URL, 368 | params={"examId": exam.id}, 369 | headers=self.get_auth_header(), 370 | ) 371 | if not r.ok: 372 | raise PageConnectionError(f"__get_subjects出错 \n {r.text}") 373 | json_data = r.json() 374 | for subject in json_data["result"]["paperList"]: 375 | subjects.append( 376 | Subject( 377 | id=subject["paperId"], 378 | name=subject["subjectName"], 379 | code=subject["subjectCode"], 380 | standard_score=subject["standardScore"], 381 | exam_id=exam.id, 382 | ) 383 | ) 384 | return subjects 385 | 386 | def get_subjects(self, exam_data: Union[Exam, str] = "") -> ExtendedList[Subject]: 387 | """获得指定考试的所有学科(不算总分) 388 | 389 | Args: 390 | exam_data (Union[Exam, str]): 考试id 或 考试名称 或 Exam实例, 默认值为最新考试 391 | 392 | Returns: 393 | ExtendedList[Subject] 394 | """ 395 | exam = self.get_exam(exam_data) 396 | if exam is None: 397 | return ExtendedList([]) 398 | return self.__get_subjects(exam) 399 | 400 | def __get_subject(self, exam: Exam, subject_data: str): 401 | self.update_login_status() 402 | subjects = self.get_subjects(exam) 403 | if _check_is_uuid(subject_data): # 判断为id还是名称 404 | subject = subjects.find_by_id(subject_data) # 为id 405 | else: 406 | subject = subjects.find_by_name(subject_data) # 为名称 407 | return subject 408 | 409 | def get_subject( 410 | self, subject_data: Union[Subject, str], exam_data: Union[Exam, str] = "" 411 | ) -> Subject: 412 | """获取指定考试的学科 413 | 414 | Args: 415 | subject_data (Union[Subject, str]): 学科id 或 学科名称, 为Subject实例时直接返回 416 | exam_data (Union[Exam, str]): 考试id 或 考试名称 或 Exam实例, 默认值为最新考试 417 | 418 | Returns: 419 | Subject 420 | """ 421 | if isinstance(subject_data, Subject): 422 | return subject_data 423 | exam = self.get_exam(exam_data) 424 | if exam is None: 425 | return Subject() 426 | subject = self.__get_subject(exam, subject_data) 427 | return subject if subject is not None else Subject() 428 | 429 | def __get_original(self, topic_set_id: str, exam_id: str) -> List[str]: 430 | self.update_login_status() 431 | r = self._session.get( 432 | Url.GET_ORIGINAL_URL, 433 | params={ 434 | "examId": exam_id, 435 | "paperId": topic_set_id, 436 | }, 437 | headers=self.get_auth_header(), 438 | ) 439 | if not r.ok: 440 | raise PageConnectionError(f"__get_original出错 \n {r.text}") 441 | json_data = r.json() 442 | if json_data["result"] == "": 443 | print(json_data) 444 | image_urls = [] 445 | for image_url in json.loads(json_data["result"]["sheetImages"]): 446 | image_urls.append(image_url) 447 | return image_urls 448 | 449 | def get_original( 450 | self, subject_data: Union[Subject, str], exam_data: Union[Exam, str] = "" 451 | ) -> List[str]: 452 | """获得指定考试学科的原卷地址 453 | 454 | Args: 455 | subject_data (Union[Subject, str]): 学科id 或 学科名称 或 Subject实例 456 | exam_data (Union[Exam, str]): 考试id 或 考试名称, 默认为最新考试 457 | 458 | Returns: 459 | List[str]: 原卷地址的列表 460 | """ 461 | exam = self.get_exam(exam_data) 462 | if not exam: 463 | return [] 464 | subject = self.get_subject(subject_data, exam) 465 | if not subject: 466 | return [] 467 | return self.__get_original(subject.id, exam.id) 468 | 469 | def __get_answer_records(self, topic_set_id: str, exam_id: str): 470 | self.update_login_status() 471 | r = self._session.get( 472 | Url.GET_ORIGINAL_URL, 473 | params={ 474 | "examId": exam_id, 475 | "paperId": topic_set_id, 476 | }, 477 | headers=self.get_auth_header(), 478 | ) 479 | if not r.ok: 480 | raise PageConnectionError(f"__get_answer_records出错 \n {r.text}") 481 | elif not r.json()["result"]: 482 | raise PageConnectionError(f"__get_answer_records出错 \n {r.json()}") 483 | json_data = json.loads(r.json()["result"]["sheetDatas"]) 484 | records = AnswerRecord() 485 | for topic in json_data["userAnswerRecordDTO"]["answerRecordDetails"]: 486 | topic_records = TopicRecord( 487 | title=topic["dispTitle"], 488 | score=topic["score"], 489 | standard_score=topic["standardScore"], 490 | subtopic_records=None 491 | ) 492 | if "subTopics" in topic: 493 | topic_records.subtopic_records = [] 494 | for subtopic in topic["subTopics"]: 495 | subtopic_record = SubTopicRecord( 496 | score=subtopic["score"], marking_records=None) 497 | if "teacherMarkingRecords" in subtopic: 498 | subtopic_record.marking_records = [ 499 | MarkingRecord( 500 | time=datetime.fromtimestamp(marking["markingTime"] / 1e3), 501 | score=marking["score"], 502 | teacher_name=marking["teacherName"] 503 | ) 504 | for marking in subtopic["teacherMarkingRecords"] 505 | ] 506 | topic_records.subtopic_records.append(subtopic_record) 507 | records.append(topic_records) 508 | return records 509 | 510 | def get_answer_records(self, subject_data: Union[Subject, str], exam_data: Union[Exam, str] = "") -> AnswerRecord: 511 | """获得指定考试学科的得分详情 512 | 513 | Args: 514 | subject_data (Union[Subject, str]): 学科id 或 学科名称 或 Subject实例 515 | exam_data (Union[Exam, str]): 考试id 或 考试名称, 默认为最新考试 516 | 517 | Returns: 518 | AnswerRecord: 得分详情 519 | """ 520 | exam = self.get_exam(exam_data) 521 | if not exam: 522 | return AnswerRecord() 523 | subject = self.get_subject(subject_data, exam) 524 | if not subject: 525 | return AnswerRecord() 526 | return self.__get_answer_records(subject.id, exam.id) 527 | 528 | def get_clazzs(self) -> ExtendedList[StuClass]: 529 | """获取当前年级所有班级""" 530 | clazzs: ExtendedList[StuClass] = ExtendedList() 531 | r = self._session.get(Url.GET_CLAZZS_URL, params={"d": int(time.time())}) 532 | if not r.ok: 533 | raise PageConnectionError(f"get_clazzs出错 \n {r.text}") 534 | json_data = r.json() 535 | for clazz in json_data["clazzs"]: 536 | clazzs.append( 537 | StuClass( 538 | name=clazz["name"], 539 | id=clazz["id"], 540 | grade=self.clazz.grade, 541 | school=self.clazz.school, 542 | ) 543 | ) 544 | return clazzs 545 | 546 | def get_clazz(self, clazz_data: Union[StuClass, str] = "") -> StuClass: 547 | """获取当前年级班级 548 | 549 | Args: 550 | clazz_data (Union[StuClass, str]): 班级id 或 班级名称, 为StuClass实例时直接返回, 为空时返回自己班级 551 | 552 | Returns: 553 | StuClass 554 | """ 555 | if not clazz_data: 556 | return self.clazz 557 | if isinstance(clazz_data, StuClass): 558 | return clazz_data 559 | clazzs = self.get_clazzs() 560 | if clazz_data.isdigit(): # 判断为id还是名称 561 | clazz = clazzs.find_by_id(clazz_data) # 为id 562 | else: 563 | clazz = clazzs.find_by_name(clazz_data) # 为名称 564 | return clazz 565 | 566 | def __get_classmates(self, clazz_id: str) -> ExtendedList[StuPerson]: 567 | self.update_login_status() 568 | classmates = ExtendedList() 569 | r = self._session.get( 570 | Url.GET_CLASSMATES_URL, 571 | params={"r": f"{self.id}student", "clazzId": clazz_id}, 572 | ) 573 | if not r.ok: 574 | raise PageConnectionError(f"__get_classmates出错 \n {r.text}") 575 | json_data = r.json() 576 | for classmate_data in json_data: 577 | classmate = StuPerson( 578 | name=classmate_data["name"], 579 | id=classmate_data["id"], 580 | clazz=StuClass( 581 | id=classmate_data["clazz"]["id"], 582 | name=classmate_data["clazz"]["name"], 583 | grade=self.clazz.grade, 584 | school=School( 585 | id=classmate_data["clazz"]["school"]["id"], 586 | name=classmate_data["clazz"]["school"]["name"], 587 | ), 588 | ), 589 | code=classmate_data.get("code"), 590 | gender=Sex.BOY if classmate_data["gender"] == "1" else Sex.GIRL, 591 | mobile=classmate_data["mobile"], 592 | ) 593 | classmates.append(classmate) 594 | return classmates 595 | 596 | def get_classmates( 597 | self, clazz_data: Union[StuClass, str] = "" 598 | ) -> ExtendedList[StuPerson]: 599 | """获取指定班级里学生列表 600 | 601 | Args: 602 | clazz_data (Union[StuClass, str]): 班级id 或 班级名称 或 StuClass实例, 为空时获取本班学生列表 603 | 604 | Returns: 605 | ExtendedList[StuPerson] 606 | """ 607 | clazz = self.get_clazz(clazz_data) 608 | if clazz is None: 609 | return ExtendedList([]) 610 | return self.__get_classmates(clazz.id) 611 | 612 | def get_homeworks( 613 | self, 614 | size: int = 20, 615 | is_complete: bool = False, 616 | subject_code: str = "-1", 617 | create_time: int = 0, 618 | ) -> ExtendedList[StuHomework]: 619 | """获取指定数量的作业(暂时不支持获取所有作业) 620 | 621 | Args: 622 | size (int): 返回的数量 623 | is_complete (bool): True 表示取已完成的作业, False 表示取未完成的作业 624 | subject_code (code): "01" 表示取语文作业, "02"表示取数学作业, 以此类推 625 | create_time (int): 取创建时间在多久以前的作业, 0表示从最新取 (暂时用不到) 626 | Returns: 627 | ExtendedList[StuHomework]: 作业(不包含作业资源) 628 | """ 629 | self.update_login_status() 630 | r = self._session.get( 631 | Url.GET_HOMEWORK_URL, 632 | params={ 633 | "pageIndex": 2, 634 | "completeStatus": 1 if is_complete else 0, 635 | "pageSize": size, # 取几个 636 | "subjectCode": subject_code, 637 | "token": self.get_auth_header()["XToken"], 638 | "createTime": create_time, # 创建时间在多久以前的 0 为从最新开始 639 | }, 640 | ) 641 | homeworks: ExtendedList[StuHomework] = ExtendedList() 642 | data = r.json()["result"] 643 | for each in data["list"]: 644 | homeworks.append( 645 | StuHomework( 646 | id=each["hwId"], 647 | title=each["hwTitle"], 648 | type=HwType( 649 | name=each["homeWorkTypeDTO"]["typeName"], 650 | code=each["homeWorkTypeDTO"]["typeCode"], 651 | ), 652 | begin_time=each["beginTime"] / 1000, 653 | end_time=each["endTime"] / 1000, 654 | create_time=each["createTime"] / 1000, 655 | is_allow_makeup=bool(each["isAllowMakeup"]), 656 | class_id=each["classId"], 657 | stu_hwid=each["stuHwId"], 658 | ) 659 | ) 660 | return homeworks 661 | 662 | def get_homework_resources(self, homework: StuHomework) -> List[HwResource]: 663 | """获取指定自由出题的作业资源(例如题目文档) 664 | 665 | Args: 666 | homework (StuHomework): 作业 667 | Returns: 668 | List[HwResource]: 作业资源 669 | """ 670 | self.update_login_status() 671 | if homework.type.code == 102: 672 | return [] 673 | r = self._session.post( 674 | Url.GET_HOMEWORK_EXERCISE_URL, 675 | json={ 676 | "base": { 677 | "appId": "WNLOIVE", 678 | "appVersion": "", 679 | "sysVersion": "v1001", 680 | "sysType": "web", 681 | "packageName": "com.iflytek.edu.hw", 682 | "udid": self.id, 683 | "expand": {}, 684 | }, 685 | "params": {"hwId": homework.id}, 686 | }, 687 | headers={ 688 | "Authorization": self.get_auth_header()["XToken"], 689 | }, 690 | ) 691 | data = r.json()["result"] 692 | resources = [] 693 | for each in data["topicAttachments"]: 694 | resources.append(HwResource(name=each["name"], path=each["path"])) 695 | return resources 696 | 697 | def get_exercise_answer(self, homework: StuHomework) -> List[HwAnswer]: 698 | """获取指定自由出题的答案 699 | Args: 700 | homework (StuHomework): 作业 701 | Returns: 702 | List[HwAnswer]: 作业答案 703 | """ 704 | self.update_login_status() 705 | r = self._session.post( 706 | Url.GET_HOMEWORK_EXERCISE_URL, 707 | json={ 708 | "base": { 709 | "appId": "WNLOIVE", 710 | "appVersion": "", 711 | "sysVersion": "v1001", 712 | "sysType": "web", 713 | "packageName": "com.iflytek.edu.hw", 714 | "udid": self.id, 715 | "expand": {}, 716 | }, 717 | "params": {"hwId": homework.id}, 718 | }, 719 | headers={ 720 | "Authorization": self.get_auth_header()["XToken"], 721 | }, 722 | ) 723 | data = r.json()["result"] 724 | ans = [] 725 | for section in data['sectionList']: 726 | for topic in section['topicList']: 727 | for child in topic['children']: 728 | title = topic['title'] 729 | content = ' '.join(child['answers']) 730 | ans.append(HwAnswer(str(title), str(content))) 731 | return ans 732 | 733 | def get_bank_answer(self, homework: StuHomework) -> List[HwAnswer]: 734 | """获取指定题库练习的答案 735 | Args: 736 | homework (StuHomework): 作业 737 | Returns: 738 | List[HwAnswer]: 作业答案 739 | """ 740 | self.update_login_status() 741 | r = self._session.post( 742 | Url.GET_HOMEWORK_BANK_URL, 743 | json={ 744 | "base": { 745 | "appId": "OAXI57PG", 746 | "appVersion": "", 747 | "sysVersion": "v1001", 748 | "sysType": "web", 749 | "packageName": "com.iflytek.edu.hw", 750 | "udid": self.id, 751 | "expand": {}, 752 | }, 753 | "params": { 754 | "classId": homework.class_id, 755 | "hwId": homework.id, 756 | }, 757 | }, 758 | headers={ 759 | "Authorization": self.get_auth_header()["XToken"], 760 | }, 761 | ) 762 | data = r.json()["result"] 763 | ans = [] 764 | for question in data['questionList']: 765 | content = '' 766 | title = question['questionTitle'] 767 | for subquestion in question['subQuestion']: 768 | content = content + ' '.join(subquestion['answer']) 769 | ans.append(HwAnswer(str(title), str(content))) 770 | return ans 771 | 772 | def get_homework_answer(self, homework: StuHomework) -> List[HwAnswer]: 773 | """获取指定作业的答案 774 | 775 | Args: 776 | homework (StuHomework): 作业 777 | Returns: 778 | List[HwBankAnswer]: 作业答案 779 | """ 780 | self.update_login_status() 781 | if homework.type.code != 105 and homework.type.code != 102: 782 | return [] 783 | if homework.type.code == 105: 784 | return self.get_exercise_answer(homework) 785 | else: 786 | return self.get_bank_answer(homework) 787 | 788 | def _set_exam_rank(self, mark: Mark, academic_year: AcademicYear): 789 | r = self._session.get( 790 | Url.GET_EXAM_LEVEL_TREND_URL, 791 | params={ 792 | "examId": mark.exam.id, 793 | "pageIndex": 1, 794 | "pageSize": 1, 795 | "startSchoolYear": academic_year.begin_time, 796 | "endSchoolYear": academic_year.end_time, 797 | }, 798 | headers=self.get_auth_header(), 799 | ) 800 | data = r.json() 801 | if data["errorCode"] != 0: 802 | return 803 | num = -1 804 | if len(data["result"]["list"]) != 0: 805 | num = data["result"]["list"][0]["dataList"][0]["totalNum"] 806 | else: 807 | subject = self.__get_subjects(mark.exam)[0].id 808 | r = self._session.get( 809 | Url.GET_PAPER_LEVEL_TREND_URL, 810 | params={ 811 | "examId": mark.exam.id, 812 | "pageIndex": 1, 813 | "pageSize": 1, 814 | "paperId": subject, 815 | "startSchoolYear": academic_year.begin_time, 816 | "endSchoolYear": academic_year.end_time, 817 | }, 818 | headers=self.get_auth_header(), 819 | ) 820 | num = r.json()["result"]["list"][0]["dataList"][0]["totalNum"] 821 | r = self._session.get( 822 | Url.GET_SUBJECT_DIAGNOSIS, 823 | params={"examId": mark.exam.id}, 824 | headers=self.get_auth_header(), 825 | ) 826 | data = r.json() 827 | if data["errorCode"] != 0: 828 | return 829 | for each in data["result"]["list"]: 830 | each_mark = mark.find(lambda t: t.subject.code == each["subjectCode"]) 831 | if each_mark is not None: 832 | each_mark.class_rank = round(num - (100 - each["myRank"]) / 100 * (num - 1)) 833 | 834 | def get_errorbook(self, exam_id, topic_set_id: str) -> List[ErrorBookTopic]: 835 | r = self._session.get( 836 | Url.GET_ERRORBOOK_URL, 837 | params={"examId": exam_id, "paperId": topic_set_id}, 838 | headers=self.get_auth_header(), 839 | ) 840 | data = r.json() 841 | if data["errorCode"] != 0: 842 | # {'errorCode': 40217, 'errorInfo': '暂时未收集到试题信息,无法查看', 'result': ''} 843 | raise Exception(data) 844 | result = [] 845 | for each in data["result"]["wrongTopicAnalysis"]["topicList"]: 846 | result.append( 847 | ErrorBookTopic( 848 | analysis_html=each["analysisHtml"], 849 | answer_html=each["answerHtml"], 850 | answer_type=each["answerType"], 851 | is_correct=each["beCorrect"], 852 | class_score_rate=each["classScoreRate"], 853 | content_html=each["contentHtml"], 854 | difficulty=each["difficultyValue"], 855 | dis_title_number=each["disTitleNumber"], 856 | image_answer=each.get("imageAnswer"), 857 | paper_id=each["paperId"], 858 | subject_name=each["paperName"], 859 | score=each["score"], 860 | standard_answer=each["standardAnswer"], 861 | standard_score=each["standardScore"], 862 | topic_analysis_img_url=each["topicAnalysisImgUrl"], 863 | topic_set_id=each["topicId"], 864 | topic_img_url=each["topicImgUrl"], 865 | topic_source_paper_name=each["topicSourcePaperName"], 866 | ) 867 | ) 868 | return result 869 | -------------------------------------------------------------------------------- /zhixuewang/student/urls.py: -------------------------------------------------------------------------------- 1 | from zhixuewang.urls import BASE_URL 2 | 3 | 4 | class Url: 5 | INFO_URL = f"{BASE_URL}/container/container/student/account/" 6 | 7 | # Login 8 | SERVICE_URL = f"{BASE_URL}:443/ssoservice.jsp" 9 | SSO_URL = f"https://open.changyan.com/sso/login?sso_from=zhixuesso&service={SERVICE_URL}" 10 | 11 | CHANGE_PASSWORD_URL = f"{BASE_URL}/portalcenter/home/updatePassword/" 12 | TEST_PASSWORD_URL = f"{BASE_URL}/weakPwdLogin/?from=web_login" 13 | 14 | TEST_URL = f"{BASE_URL}/container/container/teacher/teacherAccountNew" 15 | 16 | # Exam 17 | XTOKEN_URL = f"{BASE_URL}/container/app/token/getToken" 18 | GET_EXAM_URL = f"{BASE_URL}/zhixuebao/report/exam/getUserExamList" 19 | GET_RECENT_EXAM_URL = f"{BASE_URL}/zhixuebao/report/exam/getRecentExam" 20 | # GET_MARK_URL = f"{BASE_URL}/zhixuebao/zhixuebao/feesReport/getStuSingleReportDataForPK/" 21 | GET_SUBJECT_URL = f"{BASE_URL}/zhixuebao/report/exam/getReportMain" 22 | GET_MARK_URL = GET_SUBJECT_URL 23 | GET_ORIGINAL_URL = f"{BASE_URL}/zhixuebao/report/checksheet/" 24 | GET_ACADEMIC_YEAR_URL = f"{BASE_URL}/zhixuebao/base/common/academicYear" 25 | 26 | # Person 27 | GET_CLAZZS_URL = f"{BASE_URL}/zhixuebao/zhixuebao/friendmanage/" 28 | # GET_CLASSMATES_URL = f"{BASE_URL}/zhixuebao/zhixuebao/getClassStudent/" 29 | GET_CLASSMATES_URL = f"{BASE_URL}/container/contact/student/students" 30 | GET_TEACHERS_URL = f"{BASE_URL}/container/contact/student/teachers" 31 | 32 | APP_BASE_URL = "https://mhw.zhixue.com" 33 | # Homework 34 | GET_HOMEWORK_URL = f"{APP_BASE_URL}/homework_middle_service/stuapp/getStudentHomeWorkList" 35 | GET_HOMEWORK_EXERCISE_URL = f"{APP_BASE_URL}/hw/manage/homework/redeploy" 36 | GET_HOMEWORK_BANK_URL = f"{APP_BASE_URL}/hwreport/question/listView" 37 | 38 | GET_EXAM_LEVEL_TREND_URL = f"{BASE_URL}/zhixuebao/report/exam/getLevelTrend" 39 | 40 | GET_PAPER_LEVEL_TREND_URL = f"{BASE_URL}/zhixuebao/report/paper/getLevelTrend" 41 | GET_LOST_TOPIC_URL = f"{BASE_URL}/zhixuebao/report/paper/getExamPointsAndScoringAbility" 42 | GET_ERRORBOOK_URL = f"{BASE_URL}/zhixuebao/report/paper/getLostTopicAndAnalysis" 43 | GET_SUBJECT_DIAGNOSIS = f"{BASE_URL}/zhixuebao/report/exam/getSubjectDiagnosis" 44 | -------------------------------------------------------------------------------- /zhixuewang/teacher/__init__.py: -------------------------------------------------------------------------------- 1 | from zhixuewang.teacher.teacher import TeacherAccount 2 | 3 | __all__ = [ 4 | "TeacherAccount", 5 | 6 | ] 7 | -------------------------------------------------------------------------------- /zhixuewang/teacher/models.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | from typing import List 4 | from zhixuewang.models import ( 5 | Exam, 6 | Person, 7 | # School, 8 | StuClass, 9 | Sex, 10 | ) 11 | 12 | 13 | class TeacherRole(Enum): 14 | TEACHER = "老师" 15 | HEADMASTER = "校长" 16 | VICE_HEADMASTER = "副校长" 17 | VICE_HEADTEACHER = "副班主任" 18 | HEADTEACHER = "班主任" 19 | SCHOOL_ADMINISTRATOR = "校管理员" 20 | GRADE_DIRECTER = "年级组长" 21 | SUBJECT_LEADER = "备课组长" 22 | 23 | def __str__(self): 24 | return self._value_ 25 | 26 | 27 | class TeaPerson(Person): 28 | def __init__( 29 | self, 30 | name: str = "", 31 | id: str = "", 32 | gender: Sex = Sex.GIRL, 33 | mobile: str = "", 34 | avatar: str = "", 35 | code: str = "", 36 | clazz: StuClass = None, 37 | ): 38 | super().__init__(name, id, gender, mobile, avatar) 39 | self.code = code 40 | self.clazz = clazz 41 | 42 | 43 | @dataclass 44 | class MarkingProgress: 45 | topic_number: str 46 | complete_rate: float 47 | complete_count: int 48 | all_count: int 49 | 50 | @dataclass 51 | class PageExam: 52 | exams: List[Exam] 53 | page_index: int 54 | page_size: int 55 | all_pages: int 56 | has_next_page: bool 57 | 58 | @dataclass 59 | class AcademicInfo: 60 | term_id: str 61 | circles_year: str 62 | teaching_cycle_id: str 63 | begin_time: int 64 | end_time: int 65 | school_id: str 66 | -------------------------------------------------------------------------------- /zhixuewang/teacher/teacher.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | from zhixuewang.models import ( 3 | Account, 4 | Exam, 5 | ExtendedList, 6 | Role, 7 | School, 8 | StuClass, 9 | Subject, 10 | Grade, 11 | TextBook, 12 | StuPerson, 13 | ) 14 | from zhixuewang.teacher.models import MarkingProgress, PageExam, TeaPerson, AcademicInfo 15 | from zhixuewang.teacher.urls import Url 16 | 17 | 18 | class TeacherAccount(Account, TeaPerson): 19 | """老师账号""" 20 | 21 | teaching_classes: list = [] 22 | province: str = None 23 | city: str = None 24 | subject: Subject = None 25 | teaching_grade: Grade = None 26 | teaching_textbook: TextBook = None 27 | school: School = None 28 | 29 | def __init__(self, session): 30 | super().__init__(session, Role.teacher) 31 | self.roles = None 32 | self._token = None 33 | 34 | def set_advanced_info(self): 35 | r = self._session.get( 36 | Url.GET_ADVANCED_INFORMATION_URL, 37 | headers={ 38 | "referer": "https://www.zhixue.com/paperfresh/dist/assets/expertPaper.html" 39 | }, 40 | ) 41 | if r.status_code != 200: 42 | return self 43 | data = r.json()["result"] 44 | self.province = data["province"]["name"] if data["province"] else None 45 | self.city = data["city"]["name"] if data["city"] else None 46 | if data["school"]: 47 | self.school = School( 48 | name=data["school"]["name"], id=data["school"]["id"] 49 | ) 50 | if data["curSubject"]: 51 | self.subject = Subject( 52 | data["curSubject"]["name"], code=data["curSubject"]["code"] 53 | ) 54 | if data["grade"]: 55 | self.teaching_grade = Grade( 56 | data["grade"]["name"], code=data["grade"]["code"] 57 | ) 58 | if data["textBookVersion"]: 59 | self.teaching_textbook = TextBook( 60 | code=data["textBookVersion"]["code"], 61 | name=data["textBookVersion"]["name"], 62 | version=data["bookVersion"]["name"], 63 | versionCode=data["bookVersion"]["code"], 64 | #!TODO 暂无法通过对应的Code获取学科,暂时使用教师绑定的学科 65 | bindSubject=self.subject, 66 | ) 67 | for teaching_grade in data["curTeachingGrades"]: 68 | for clazz in teaching_grade["clazzs"]: 69 | #!TODO 暂无法获取班级所在学校,暂时使用教师绑定的学校 70 | self.teaching_classes.append( 71 | StuClass( 72 | id=clazz["code"], 73 | name=clazz["name"], 74 | grade=Grade( 75 | name=teaching_grade["name"], code=teaching_grade["code"] 76 | ), 77 | school=self.school, 78 | ) 79 | ) 80 | return self 81 | 82 | def set_base_info(self): 83 | r = self._session.get( 84 | Url.TEST_URL, 85 | headers={ 86 | "referer": "https://www.zhixue.com/container/container/teacher/index/" 87 | }, 88 | ) 89 | json_data = r.json()["teacher"] 90 | self.id = json_data.get("id") 91 | self.mobile = json_data.get("mobile") 92 | self.name = json_data.get("name") 93 | self.roles = json_data.get("roles") 94 | return self 95 | 96 | def get_school_exam_classes( 97 | self, school_id: str, topic_set_id: str 98 | ) -> List[StuClass]: 99 | self.update_login_status() 100 | r = self._session.get( 101 | Url.GET_EXAM_SCHOOLS_URL, 102 | params={"schoolId": school_id, "markingPaperId": topic_set_id}, 103 | ) 104 | data = r.json() 105 | if data is None: 106 | return [] 107 | classes = [] 108 | 109 | for each in data: 110 | classes.append( 111 | StuClass( 112 | id=each["classId"], 113 | name=each["className"], 114 | school=School(id=each["schoolId"]), 115 | ) 116 | ) 117 | return classes 118 | 119 | 120 | def get_original_paper( 121 | self, user_id: str, paper_id: str, save_to_path: str 122 | ) -> bool: 123 | """ 124 | 获得原卷 125 | Args: 126 | user_id (str): 为需要查询原卷的userId 127 | paper_id (str): 为需要查询的学科ID(topicSetId) 128 | save_to_path (str): 为原卷保存位置(html文件), 精确到文件名 129 | Return: 130 | bool: 正常会返回True 131 | """ 132 | data = self._session.get( 133 | Url.ORIGINAL_PAPER_URL, params={"userId": user_id, "paperId": paper_id} 134 | ) 135 | with open(save_to_path, encoding="utf-8", mode="w+") as fhandle: 136 | fhandle.writelines( 137 | data.text.replace("//static.zhixue.com", "https://static.zhixue.com") 138 | ) 139 | return True 140 | 141 | def get_teacher_roleText(self) -> List[str]: 142 | """ 143 | 获得教师的角色文本 144 | Return: 145 | List[str]: 教师的所有角色名称(忽略未知的教师角色) 146 | """ 147 | str_roles = [] 148 | role_table = { 149 | "teacher": "教师", 150 | "subjectLeader": "备课组长", 151 | "gradeDirecter": "年级组长", 152 | "headteacher": "班主任", 153 | "headmaster": "校长", 154 | "viceHeadteacher": "副班主任", 155 | "viceHeadmaster": "副校长", 156 | "schoolAdministrator": "校管理员", 157 | } 158 | for role in self.roles: 159 | str_role = role_table.get(role) 160 | if str_role is None: 161 | print(f"教师角色{role}未知。已忽略。") 162 | else: 163 | str_roles.append(str_role) 164 | return str_roles 165 | 166 | def get_exam_subjects(self, exam_id: str) -> ExtendedList[Subject]: 167 | """ 168 | 获取某个考试的考试科目 169 | Args: 170 | exam_id (str): 为需要查询考试的id 171 | Return: 172 | bool: 正常会返回True 173 | """ 174 | self.update_login_status() 175 | r = self._session.get(Url.GET_EXAM_SUBJECTS_URL, params={"examId": exam_id}) 176 | data = r.json()["result"] 177 | subjects = [] 178 | for each in data: 179 | name = each["subjectName"] 180 | if name != "总分" and (not each.get("isSubjectGroup")): # 排除学科组() 181 | subjects.append( 182 | Subject( 183 | id=each["topicSetId"], 184 | name=each["subjectName"], 185 | code=each["subjectCode"], 186 | standard_score=each["standScore"], 187 | ) 188 | ) 189 | return ExtendedList(sorted(subjects, key=lambda x: x.code, reverse=False)) 190 | 191 | def get_exam_detail(self, exam_id: str) -> Exam: 192 | """ 193 | 获取某个考试的详细情况 194 | 包括参考学校和考试科目 195 | Args: 196 | exam_id (str): 为需要查询考试的id 197 | Return: 198 | Exam 199 | """ 200 | self.update_login_status() 201 | r = self._session.post(Url.GET_EXAM_DETAIL_URL, data={"examId": exam_id}) 202 | data = r.json()["result"] 203 | exam = Exam() 204 | schools: ExtendedList[School] = ExtendedList() 205 | for each in data["schoolList"]: 206 | schools.append(School(id=each["schoolId"], name=each["schoolName"])) 207 | exam.id = exam_id 208 | exam.name = data["exam"]["examName"] 209 | exam.grade_code = data["exam"]["gradeCode"] 210 | 211 | exam.schools = schools 212 | exam.status = str(data["exam"]["isCrossExam"]) 213 | exam.subjects = self.get_exam_subjects(exam_id) 214 | return exam 215 | 216 | def get_marking_progress( 217 | self, 218 | topic_set_id: str, 219 | ) -> List[MarkingProgress]: 220 | """ 221 | 获取某场考试指定科目阅卷情况 222 | Args: 223 | topic_set_id (str): 科目id 224 | Return: 225 | List[MarkingProgress] 226 | """ 227 | r = self._session.post( 228 | "https://pt-ali-bj-re.zhixue.com/marking/marking/markingTopicProgress/", 229 | data={"markingPaperId": topic_set_id}, 230 | headers={"token": self.get_token()}, 231 | ) 232 | data = r.json() 233 | result = [] 234 | for each in data: 235 | result.append( 236 | MarkingProgress( 237 | topic_number=each["topicNumber"], 238 | complete_rate=each["comleteRate"], 239 | complete_count=each["completeCount"], 240 | all_count=each["allCount"], 241 | ) 242 | ) 243 | return result 244 | 245 | def _get_academic_info(self) -> List[AcademicInfo]: 246 | """ 247 | 获取学术信息用以获取教师考试 248 | """ 249 | r = self._session.get(Url.GET_AcademicTermTeachingCycle_URL) 250 | data = r.json()["result"] 251 | result = [] 252 | for did in data["termTeachingCycleMap"]: 253 | d = data["termTeachingCycleMap"][did][0] 254 | result.append( 255 | AcademicInfo( 256 | teaching_cycle_id=d["id"], 257 | circles_year=str(did), 258 | term_id=d["termId"], 259 | begin_time=d["beginTime"], 260 | end_time=d["endTime"], #! 这两个都使用Unix时间戳,单位ms 261 | school_id=data["schoolId"], 262 | ) 263 | ) 264 | result = sorted(result, key=lambda _: _.begin_time, reverse=True) 265 | return result 266 | 267 | def get_exams( 268 | self, 269 | year: int = 0, 270 | index: int = 1, 271 | class_id: str = "all", 272 | exam_name: str = "", 273 | grade_code: str = "all", 274 | subject_code: str = "all", 275 | exam_type_code: str = "all", 276 | page_size: int = 15, 277 | page_index: int = 1, 278 | ) -> PageExam: 279 | """ 280 | 获取考试, 有学年和学期两种查询方式 281 | 默认获取最新学期的考试 282 | `year`和`index`只需要传一个即可,均传默认使用`year` 283 | Args: 284 | year (int): 需要查询的年级, 如2022级则传入2022 285 | index (int): 查询距离现在第几个学期, 如传入3表示获取上三个学期的考试 286 | class_id (str): 指定查看考试的班级, 默认为全部班级 287 | exam_name (str): 指定需要查看的考试名称 288 | grade_code (str): 指定查看考试的年级 289 | subject_code (str): 指定查看考试的学科类型 290 | exam_type_code (str): 指定查看考试的类型,默认为全部 291 | page_size (int): 指定一页考试数 292 | page_index (int): 指定页数 293 | Return: 294 | PageExam: 考试信息和页数信息 295 | """ 296 | params_data = { 297 | "examName": exam_name, 298 | "gradeCode": grade_code, 299 | "classId": class_id, 300 | "subjectCode": subject_code, 301 | "examTypeCode": exam_type_code, 302 | "pageSize": page_size, 303 | "pageIndex": page_index, 304 | } 305 | if year == 0: 306 | #! 按 学期 查询 307 | academic_infos = self._get_academic_info() 308 | academic_info = academic_infos[index - 1] 309 | params_data.update( 310 | { 311 | "searchType": "schoolYearType", 312 | "circlesYear": academic_info.circles_year, 313 | "examTypeCode": exam_type_code, 314 | "termId": academic_info.term_id, 315 | "teachingCycleId": academic_info.teaching_cycle_id, 316 | "startTime": academic_info.begin_time, 317 | "endTime": academic_info.end_time, 318 | } 319 | ) 320 | r = self._session.get(Url.GET_EXAMS_URL, params=params_data) 321 | else: 322 | # 按 学级 查询 323 | params_data.update( 324 | { 325 | "searchType": "circlesType", 326 | "circlesYear": year, 327 | "termId": "", 328 | "teachingCycleId": "", 329 | "pageSize": page_size, 330 | "pageIndex": page_index, 331 | } 332 | ) 333 | r = self._session.get(Url.GET_EXAMS_URL, params=params_data) 334 | exams = [] 335 | data = r.json()["result"] 336 | if "classPaperSummaryList" not in data: 337 | return PageExam([], page_index, page_size, 0, False) 338 | for each in data["classPaperSummaryList"]: 339 | exams.append( 340 | Exam( 341 | id=each["data"]["examId"], 342 | name=each["data"]["examName"], 343 | grade_code=each["data"]["gradeCode"], 344 | subjects=ExtendedList( 345 | [ 346 | Subject(name=one["name"]) 347 | for one in each["zxSubjects"] 348 | if not one["isMultiSubject"] # 排除复合学科如理综 349 | ] 350 | ), 351 | create_time=each["data"]["createDateTime"] / 1000, 352 | is_final=each["data"]["isFinal"], 353 | ) 354 | ) 355 | return PageExam( 356 | exams=exams, 357 | page_index=page_index, 358 | page_size=page_size, 359 | all_pages=data["pageInfo"]["allPages"][-1], 360 | has_next_page=page_index < data["pageInfo"]["allPages"][-1], 361 | ) 362 | 363 | def get_token(self) -> str: 364 | if self._token is not None: 365 | return self._token 366 | self._token = self._session.get(Url.GET_TOKEN_URL).json()["result"] 367 | return self._token 368 | 369 | def get_session(self): 370 | return self._session 371 | -------------------------------------------------------------------------------- /zhixuewang/teacher/urls.py: -------------------------------------------------------------------------------- 1 | from zhixuewang.urls import BASE_URL 2 | 3 | 4 | class Url: 5 | INFO_URL = f"{BASE_URL}/container/container/student/account/" 6 | 7 | CHANGE_PASSWORD_URL = f"{BASE_URL}/portalcenter/home/updatePassword/" 8 | 9 | TEST_URL = f"{BASE_URL}/container/container/teacher/teacherAccountNew" 10 | 11 | GET_EXAM_URL = f"{BASE_URL}/classreport/class/classReportList/" 12 | GET_AcademicTermTeachingCycle_URL = f"{BASE_URL}/api-classreport/class/getAcademicTermTeachingCycle/" 13 | 14 | GET_REPORT_URL = f"{BASE_URL}/exportpaper/class/getExportStudentInfo" 15 | GET_MARKING_PROGRESS_URL = f"{BASE_URL}/marking/marking/markingProgressDetail" 16 | 17 | GET_EXAMS_URL = f"{BASE_URL}/api-classreport/class/classReportList/" 18 | GET_EXAM_DETAIL_URL = f"{BASE_URL}/scanmuster/cloudRec/scanrecognition" 19 | 20 | GET_EXAM_SCHOOLS_URL = f"{BASE_URL}/exam/marking/schoolClass" 21 | GET_EXAM_SUBJECTS_URL = f"{BASE_URL}/configure/class/getSubjectsIncludeSubAndGroup" 22 | # 后必须接上paperId 23 | # ORIGINAL_PAPER_URL = f"{BASE_URL}/classreport/class/student/checksheet/?userId=" 24 | ORIGINAL_PAPER_URL = f"{BASE_URL}/classreport/class/student/checksheet/" 25 | 26 | GET_ADVANCED_INFORMATION_URL = f"{BASE_URL}/paperfresh/api/common/getCurrentUser" 27 | GET_STUDENT_STATUS_URL = f"{BASE_URL}/api-teacher/home/getStudentStatus" 28 | 29 | GET_TOKEN_URL = f"{BASE_URL}/container/app/token/getToken" -------------------------------------------------------------------------------- /zhixuewang/tools/__init__.py: -------------------------------------------------------------------------------- 1 | from zhixuewang.tools.datetime_tool import get_property 2 | 3 | __all__ = [ 4 | "get_property", 5 | ] 6 | -------------------------------------------------------------------------------- /zhixuewang/tools/datetime_tool.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | def timestamp2datetime(timestamp: float) -> datetime.datetime: 5 | return datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=timestamp) 6 | 7 | 8 | def get_property(arg_name: str) -> property: 9 | def setter(self, mill_timestamp): 10 | self.__dict__[arg_name] = timestamp2datetime(mill_timestamp / 1000) 11 | 12 | return property(fget=lambda self: self.__dict__[arg_name], 13 | fset=setter) 14 | -------------------------------------------------------------------------------- /zhixuewang/urls.py: -------------------------------------------------------------------------------- 1 | BASE_URL = "https://www.zhixue.com" 2 | 3 | 4 | class Url: 5 | SERVICE_URL = f"{BASE_URL}:443/ssoservice.jsp" 6 | SSO_URL = f"https://sso.zhixue.com/sso_alpha/login?service={SERVICE_URL}" 7 | TEST_PASSWORD_URL = f"{BASE_URL}/weakPwdLogin/?from=web_login" 8 | TEST_URL = f"{BASE_URL}/container/container/teacher/teacherAccountNew" 9 | GET_LOGIN_STATE = f"{BASE_URL}/loginState/" 10 | --------------------------------------------------------------------------------