├── .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 |   
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 | [](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 |
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 |
--------------------------------------------------------------------------------