From 4185141e7ed70e45c7c9dac1946270c66968199e Mon Sep 17 00:00:00 2001
From: EchoZenith <60392923+EchoZenith@users.noreply.github.com>
Date: Mon, 4 May 2026 23:07:19 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=B6=85=E6=98=9F?=
=?UTF-8?q?=E5=AD=A6=E4=B9=A0=E9=80=9A=E8=87=AA=E5=8A=A8=E8=AF=84=E6=95=99?=
=?UTF-8?q?=E8=84=9A=E6=9C=AC=E5=8F=8A=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
实现超星学习通自动评教功能,支持账号密码登录和二维码登录
添加配置文件模板、依赖文件及gitignore配置
---
.gitignore | 30 ++
chaoxing_evaluate.py | 676 +++++++++++++++++++++++++++++++++++++++++
data/config.example.py | 34 +++
requirements.txt | 2 +
4 files changed, 742 insertions(+)
create mode 100644 .gitignore
create mode 100644 chaoxing_evaluate.py
create mode 100644 data/config.example.py
create mode 100644 requirements.txt
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..db4e5b4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,30 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+*.egg-info/
+dist/
+build/
+*.egg
+
+# Virtual environment
+venv/
+env/
+.venv/
+.env/
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+*~
+.DS_Store
+
+# Project specific - config with sensitive credentials
+data/config.py
+data/config.json
+
+# Project specific - generated QR code images
+data/*.png
diff --git a/chaoxing_evaluate.py b/chaoxing_evaluate.py
new file mode 100644
index 0000000..c4cfc41
--- /dev/null
+++ b/chaoxing_evaluate.py
@@ -0,0 +1,676 @@
+import re
+import json
+import sys
+import base64
+import os
+import requests # pyright: ignore[reportMissingModuleSource]
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), "data"))
+import config as cfg # pyright: ignore[reportMissingImports]
+
+try:
+ from Crypto.Cipher import AES # pyright: ignore[reportMissingImports]
+except ImportError:
+ AES = None
+
+
+AES_KEY = "u2oh6Vu^HWe4_AES"
+LOGIN_PAGE = "https://passport2.chaoxing.com/login?fid=&newversion=true&refer=https%3A%2F%2Fi.chaoxing.com"
+
+
+def aes_encrypt(text: str) -> str:
+ key = AES_KEY.encode("utf-8")
+ iv = key
+ raw = text.encode("utf-8")
+ pad_len = AES.block_size - len(raw) % AES.block_size
+ raw += bytes([pad_len] * pad_len)
+ cipher = AES.new(key, AES.MODE_CBC, iv)
+ encrypted = cipher.encrypt(raw)
+ return base64.b64encode(encrypted).decode("utf-8")
+
+
+class ChaoxingEvaluator:
+ def __init__(self, cookies: dict = None):
+ self.session = requests.Session()
+ if cookies:
+ self.session.cookies.update(cookies)
+ self.session.headers.update({
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
+ "(KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 Edg/147.0.0.0",
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,"
+ "image/avif,image/webp,image/apng,*/*;q=0.8",
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
+ })
+ self.base_url = "https://newes.chaoxing.com"
+
+ @classmethod
+ def login(cls, username: str, password: str) -> "ChaoxingEvaluator":
+ if AES is None:
+ raise ImportError("请先安装 pycryptodome: pip3 install pycryptodome")
+
+ session = requests.Session()
+ session.headers.update({
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
+ "(KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 Edg/147.0.0.0",
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
+ })
+
+ login_page = "https://passport2.chaoxing.com/login?fid=&newversion=true&refer=https%3A%2F%2Fi.chaoxing.com"
+ print("访问登录页面...")
+ session.get(login_page)
+
+ encrypted_uname = aes_encrypt(username)
+ encrypted_pwd = aes_encrypt(password)
+
+ login_data = {
+ "fid": "-1",
+ "uname": encrypted_uname,
+ "password": encrypted_pwd,
+ "refer": "https%253A%252F%252Fi.chaoxing.com",
+ "t": "true",
+ "forbidotherlogin": "0",
+ "validate": "",
+ "doubleFactorLogin": "0",
+ "independentId": "0",
+ "independentNameId": "0",
+ }
+
+ login_url = "https://passport2.chaoxing.com/fanyalogin"
+ headers = {
+ "X-Requested-With": "XMLHttpRequest",
+ "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
+ "Referer": login_page,
+ "Origin": "https://passport2.chaoxing.com",
+ }
+
+ print("正在登录...")
+ resp = session.post(login_url, data=login_data, headers=headers)
+ result = resp.json()
+
+ if not result.get("status"):
+ print(f"登录失败: {result}")
+ raise Exception(f"登录失败: {result}")
+
+ print("登录成功!")
+
+ evaluator = cls.__new__(cls)
+ evaluator.session = session
+ evaluator.base_url = "https://newes.chaoxing.com"
+
+ cfg.save_cookies(dict(session.cookies))
+ return evaluator
+
+ @classmethod
+ def qr_login(cls) -> "ChaoxingEvaluator":
+ import time
+ import subprocess
+
+ session = requests.Session()
+ session.headers.update({
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
+ "(KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 Edg/147.0.0.0",
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
+ })
+
+ print("获取登录页面...")
+ resp = session.get(LOGIN_PAGE)
+
+ uuid_match = re.search(r'id="uuid"\s*value="([^"]+)"', resp.text) or \
+ re.search(r'value="([^"]+)"\s+id="uuid"', resp.text)
+ enc_match = re.search(r'id="enc"\s*value="([^"]+)"', resp.text) or \
+ re.search(r'value="([^"]+)"\s+id="enc"', resp.text)
+ if not uuid_match or not enc_match:
+ raise Exception("无法获取二维码参数")
+
+ uuid = uuid_match.group(1)
+ enc = enc_match.group(1)
+
+ qr_url = f"https://passport2.chaoxing.com/createqr?uuid={uuid}&fid=-1"
+ qr_resp = session.get(qr_url, headers={"Referer": LOGIN_PAGE})
+ qr_data = qr_resp.content
+
+ qr_file = os.path.join(os.path.dirname(__file__), "data", "chaoxing_qr.png")
+ with open(qr_file, "wb") as f:
+ f.write(qr_data)
+ print(f"二维码已保存到: {qr_file}")
+ print("请使用超星学习通APP扫码登录...")
+
+ if sys.platform == "darwin":
+ subprocess.Popen(["open", qr_file])
+ elif sys.platform == "win32":
+ os.startfile(qr_file)
+ else:
+ print(f"请打开文件扫码: {qr_file}")
+
+ auth_url = "https://passport2.chaoxing.com/getauthstatus/v2"
+ headers = {
+ "X-Requested-With": "XMLHttpRequest",
+ "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
+ "Referer": LOGIN_PAGE,
+ "Origin": "https://passport2.chaoxing.com",
+ }
+
+ for attempt in range(180):
+ try:
+ time.sleep(2)
+ data = {"enc": enc, "uuid": uuid,
+ "doubleFactorLogin": "0", "forbidotherlogin": "0"}
+ poll_resp = session.post(auth_url, data=data, headers=headers)
+ result = poll_resp.json()
+ except KeyboardInterrupt:
+ print("\n用户中断扫码")
+ raise
+
+ if result.get("status"):
+ print(f"扫码登录成功!")
+ evaluator = cls.__new__(cls)
+ evaluator.session = session
+ evaluator.base_url = "https://newes.chaoxing.com"
+ cfg.save_cookies(dict(session.cookies))
+ return evaluator
+
+ msg = result.get("mes", "")
+ if msg == "已扫描":
+ nickname = result.get("nickname", "")
+ print(f"\r手机端已扫描,请在APP上确认登录 ({nickname})", end="", flush=True)
+ elif msg == "验证通过":
+ continue
+ elif attempt < 3:
+ print(f"\r等待扫码... ({attempt + 1}/3)", end="", flush=True)
+
+ raise Exception("二维码登录超时")
+
+ @classmethod
+ def auto(cls) -> "ChaoxingEvaluator":
+ cookies = cfg.load_cookies()
+ if cookies:
+ evaluator = cls(cookies)
+ if evaluator.check_login_valid():
+ print("使用已保存的Cookie登录")
+ return evaluator
+ print("Cookie已失效,重新登录")
+ cfg.clear_cookies()
+
+ if cfg.QR_LOGIN:
+ return cls.qr_login()
+
+ if cfg.USERNAME and cfg.PASSWORD:
+ try:
+ return cls.login(cfg.USERNAME, cfg.PASSWORD)
+ except Exception as e:
+ print(f"密码登录失败: {e}")
+ print("降级到二维码登录...")
+ return cls.qr_login()
+
+ return cls.qr_login()
+
+ def check_login_valid(self) -> bool:
+ try:
+ resp = self._get("/pj/semesterV2/findSemesterList",
+ referer=f"{self.base_url}/pj/frontv2/evaluateList/whatIEvaluated?_CP_=pj",
+ accept_json=True)
+ return resp.status_code == 200 and resp.json().get("status") == True
+ except Exception:
+ return False
+
+ def get_cookies_dict(self) -> dict:
+ return dict(self.session.cookies)
+
+ def _get(self, path: str, params: dict = None, referer: str = None, accept_json: bool = False):
+ url = f"{self.base_url}{path}"
+ headers = {}
+ if referer:
+ headers["Referer"] = referer
+ if accept_json:
+ headers["Accept"] = "application/json, text/plain, */*"
+ return self.session.get(url, params=params, headers=headers)
+
+ def _post(self, path: str, data=None, params: dict = None, referer: str = None):
+ url = f"{self.base_url}{path}"
+ headers = {
+ "X-Requested-With": "XMLHttpRequest",
+ "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
+ }
+ if referer:
+ headers["Referer"] = referer
+ return self.session.post(url, data=data, params=params, headers=headers)
+
+ def get_questionnaire_info(self, questionnaire_id: int) -> dict:
+ path = "/pj/sendClassroomLogV2/getQuestionnaire"
+ resp = self._get(path, params={"questionnaireId": questionnaire_id},
+ referer=f"{self.base_url}/pj/frontv2/evaluateList/whatIEvaluated?_CP_=pj",
+ accept_json=True)
+ return resp.json()
+
+ def get_evaluation_list(self, questionnaire_id: int, page: int = 1, page_size: int = 100) -> dict:
+ path = "/pj/newesReceptionV2/GetMyEvaluationQuestionnaireById"
+ params = {"questionnaireId": questionnaire_id, "pageIndex": page, "pageSize": page_size, "kw": ""}
+ referer = f"{self.base_url}/pj/frontv2/whatIEvaluatedDetails?_CP_=pj&questionnaireId={questionnaire_id}"
+ resp = self._get(path, params=params, referer=referer, accept_json=True)
+ return resp.json()
+
+ def get_questionnaire_page(self, fid: int, uid: int, already_id: int, grant_id: int,
+ questionnaire_id: int, classification_id: str = "") -> str:
+ path = "/pj/newesReception/questionnaireInfo"
+ params = {
+ "fid": fid, "uId": uid, "alreadyId": already_id, "grantId": grant_id,
+ "questionnaireId": questionnaire_id, "type": 1, "classificationId": classification_id,
+ "isNewPage": "true", "source": 14,
+ }
+ referer = (f"{self.base_url}/pj/frontv2/whatIEvaluatedDetails?_CP_=pj"
+ f"&questionnaireId={questionnaire_id}"
+ f"&title=%E8%AF%84%E4%BB%B7%E8%A1%A8%E8%AF%A6%E6%83%85"
+ f"&evaluateObjType=1&questionnaireType=2&sendType=18"
+ f"&hasFinished=0&questionnaireStatus=0&showCourseProfile=0"
+ f"&isShowTeacherPhoto=0&alreadyType=6&classGrade=0"
+ f"&hideScoreLevel=0&optionrepeatcheck=0&granterType=8")
+ resp = self._get(path, params=params, referer=referer)
+ return resp.text
+
+ @staticmethod
+ def parse_questions(html: str) -> list:
+ questions = []
+
+ input_pattern = re.compile(
+ r']*name=["\']groupTargetIds["\'][^>]*value=["\'](\d+)["\']'
+ )
+ type_pattern = re.compile(
+ r']*name=["\'](\d+)_type["\'][^>]*value=["\'](\d+)["\']'
+ )
+ maxscore_pattern = re.compile(
+ r']*\bid=["\'](\d+)["\'][^>]*\bmaxScore=["\']([\d.]+)["\']'
+ )
+ minscore_pattern = re.compile(
+ r']*\bid=["\'](\d+)["\'][^>]*\bminScore=["\']([\d.]+)["\']'
+ )
+
+ target_ids = input_pattern.findall(html)
+ type_matches = {m[0]: m[1] for m in type_pattern.findall(html)}
+ maxscore_matches = {m[0]: float(m[1]) for m in maxscore_pattern.findall(html)}
+ minscore_matches = {m[0]: float(m[1]) for m in minscore_pattern.findall(html)}
+
+ seen = set()
+ for tid in target_ids:
+ if tid in seen:
+ continue
+ seen.add(tid)
+ ttype = int(type_matches.get(tid, 0))
+ item = {"id": tid, "type": ttype}
+ if ttype == 5:
+ item["maxScore"] = maxscore_matches.get(tid, 0)
+ item["minScore"] = minscore_matches.get(tid, 0)
+ questions.append(item)
+
+ return questions
+
+ def check_score_is_rightful(self, questionnaire_id: int, target_id: int, uid: int, score: float,
+ referer: str) -> bool:
+ path = "/pj/newesReception/checkScoreIsRightful"
+ data = {"questionnaireId": questionnaire_id, "targetId": target_id,
+ "uId": uid, "score": score}
+ resp = self._post(path, data=data, referer=referer)
+ result = resp.json()
+ return not result.get("flag", True)
+
+ def check_no_answer_target(self, questionnaire_id: int, total_score: float,
+ no_answer_ids: list, referer: str) -> dict:
+ path = "/pj/newesReception/checkNoAnswerTarget"
+ data = {
+ "questionnaireId": questionnaire_id,
+ "questionnaireScore": total_score,
+ "noAnswerTargetIds": json.dumps(no_answer_ids),
+ }
+ resp = self._post(path, data=data, referer=referer)
+ return resp.json()
+
+ def check_total_score_is_rightful(self, questionnaire_id: int, uid: int,
+ score: float, referer: str) -> bool:
+ path = "/pj/newesReception/checkTotalScoreIsRightful"
+ data = {"questionnaireId": questionnaire_id, "uId": uid, "score": score}
+ resp = self._post(path, data=data, referer=referer)
+ result = resp.json()
+ return not result.get("flag", True)
+
+ def save_questionnaire(self, fid: int, uid: int, questionnaire_id: int,
+ already_id: int, grant_id: int,
+ questions: list, total_score: float,
+ referer: str) -> dict:
+ path = "/pj/newesReception/saveQuestionnaire"
+ params = {
+ "checkScore": total_score,
+ "questionnaireId": questionnaire_id,
+ "fid": fid,
+ "uId": uid,
+ "grantId": grant_id,
+ "alreadyId": already_id,
+ }
+
+ form_data = [
+ ("uId", str(uid)),
+ ("fid", str(fid)),
+ ("questionnaireId", str(questionnaire_id)),
+ ("alreadyId", str(already_id)),
+ ("grantId", str(grant_id)),
+ ]
+
+ for q in questions:
+ qid_str = str(q["id"])
+ form_data.append(("groupTargetIds", qid_str))
+ form_data.append((f"{qid_str}_type", str(q["type"])))
+ form_data.append((f"{qid_str}_chooseSetUp", "1"))
+ if q["type"] == 5:
+ score = q["maxScore"]
+ form_data.append((qid_str, str(score)))
+ elif q["type"] == 4:
+ form_data.append(("jumpInfo", ""))
+ form_data.append((qid_str, ""))
+
+ form_data.append(("saveType", "2"))
+ form_data.append(("submitreasons", ""))
+ form_data.append(("submit_highscore_reasons", ""))
+ form_data.append(("submit_lowscore_reasons", ""))
+ form_data.append(("checkScore", str(total_score)))
+
+ resp = self._post(path, data=form_data, params=params, referer=referer)
+ return resp.json()
+
+ def evaluate_single(self, fid: int, uid: int, already_id: int, grant_id: int,
+ questionnaire_id: int, teacher_name: str = "", course_name: str = "") -> bool:
+ print(f"\n{'='*60}")
+ print(f"正在评教: {teacher_name} - {course_name}")
+
+ referer = (f"{self.base_url}/pj/newesReception/questionnaireInfo"
+ f"?fid={fid}&uId={uid}&alreadyId={already_id}"
+ f"&grantId={grant_id}&questionnaireId={questionnaire_id}"
+ f"&type=1&classificationId=&isNewPage=true&source=14")
+
+ html = self.get_questionnaire_page(fid, uid, already_id, grant_id, questionnaire_id)
+ questions = self.parse_questions(html)
+
+ if not questions:
+ print("未解析到任何题目,可能HTML结构已变更")
+ return False
+
+ print(f"解析到 {len(questions)} 道题目:")
+ total_score = 0
+ has_scoring = False
+ for q in questions:
+ if q["type"] == 5:
+ has_scoring = True
+ print(f" 打分题 [{q['id']}] 满分 {q['maxScore']} -> 给满分")
+ total_score += q["maxScore"]
+ elif q["type"] == 4:
+ print(f" 简答题 [{q['id']}] -> 留空")
+
+ if has_scoring and total_score == 0:
+ print("所有打分题满分为0,页面可能为只读状态(已评价),跳过")
+ return False
+
+ print(f"总分: {total_score}")
+
+ no_answer_ids = [str(q["id"]) for q in questions if q["type"] != 5]
+
+ print("正在验证各题分数...")
+ all_score_ok = True
+ for q in questions:
+ if q["type"] == 5:
+ score = q["maxScore"]
+ ok = self.check_score_is_rightful(questionnaire_id, int(q["id"]), uid, score, referer)
+ if not ok:
+ print(f" 题目 {q['id']} 分数验证未通过")
+ all_score_ok = False
+ else:
+ print(f" 题目 {q['id']} 分数验证通过")
+
+ if not all_score_ok:
+ print("分数验证失败,停止提交")
+ return False
+
+ print("正在检查未答题目...")
+ no_answer_result = self.check_no_answer_target(
+ questionnaire_id, total_score, no_answer_ids, referer
+ )
+ print(f" 未答检查结果: {no_answer_result}")
+
+ print("正在验证总分...")
+ total_ok = self.check_total_score_is_rightful(questionnaire_id, uid, total_score, referer)
+ if not total_ok:
+ print("总分验证未通过")
+ return False
+ print(" 总分验证通过")
+
+ print("正在提交评教...")
+ result = self.save_questionnaire(
+ fid, uid, questionnaire_id, already_id, grant_id,
+ questions, total_score, referer
+ )
+ print(f"提交结果: {result}")
+
+ if result.get("status") == 1:
+ print(f"评教成功: {teacher_name} - {course_name}")
+ return True
+ else:
+ print(f"评教失败: {result}")
+ return False
+
+ def discover_questionnaires(self) -> list:
+ semester_resp = self._get("/pj/semesterV2/findSemesterList",
+ referer=f"{self.base_url}/pj/frontv2/evaluateList/whatIEvaluated?_CP_=pj",
+ accept_json=True)
+ semester_id = None
+ try:
+ semesters = semester_resp.json()
+ if semesters.get("status"):
+ for s in semesters.get("data", []):
+ if s.get("iscurrent") == 1:
+ semester_id = s.get("id")
+ break
+ if semester_id is None and semesters.get("data"):
+ semester_id = semesters["data"][0].get("id")
+ except (json.JSONDecodeError, ValueError, TypeError):
+ pass
+
+ if not semester_id:
+ print("无法获取学期ID")
+ return []
+
+ list_resp = self._get("/pj/newesReceptionV2/GetMyEvaluationList",
+ params={"evaluateObjType": "", "semesterId": semester_id,
+ "title": "", "sort": 2, "pageIndex": 1, "pageSize": 20},
+ referer=f"{self.base_url}/pj/frontv2/evaluateList/whatIEvaluated?_CP_=pj",
+ accept_json=True)
+ questionnaires = []
+ try:
+ data = list_resp.json()
+ if not data.get("status") and data.get("code") != 1:
+ return []
+ items = []
+ if data.get("status"):
+ items = data.get("data", {}).get("list", [])
+ elif data.get("code") == 1:
+ items = data.get("data", {}).get("list", [])
+
+ for q in items:
+ qid = q.get("questionnaireId")
+ name = q.get("title") or q.get("name", "")
+ if qid:
+ questionnaires.append({"id": int(qid), "name": name})
+ except (json.JSONDecodeError, ValueError, TypeError):
+ pass
+
+ return questionnaires
+
+ def batch_evaluate(self, questionnaire_id: int, fid: int = None, uid: int = None):
+ info = self.get_questionnaire_info(questionnaire_id)
+ if info.get("status") != 1:
+ print("获取问卷信息失败")
+ return
+
+ eval_list_resp = self.get_evaluation_list(questionnaire_id)
+ if not eval_list_resp.get("status"):
+ print("获取评教列表失败")
+ return
+
+ data = eval_list_resp.get("data", {})
+ total = data.get("count", 0)
+ items = data.get("list", [])
+
+ print(f"共有 {total} 条待评记录")
+
+ success = 0
+ fail = 0
+ for i, item in enumerate(items, 1):
+ obj = item.get("alreadyObject", {})
+ teacher_name = obj.get("teacherName", "未知")
+ course_name = obj.get("courseName", "未知")
+
+ already_id = item.get("alreadyObjectId")
+ grant_id = item.get("grantId")
+ _fid = fid or obj.get("fid") or info.get("classroomlog", {}).get("fid")
+ _uid = uid or int(self.session.cookies.get("_uid", 0))
+
+ print(f"\n[{i}/{total}] ", end="")
+ ok = self.evaluate_single(
+ fid=_fid, uid=_uid,
+ already_id=already_id, grant_id=grant_id,
+ questionnaire_id=questionnaire_id,
+ teacher_name=teacher_name, course_name=course_name,
+ )
+ if ok:
+ success += 1
+ else:
+ fail += 1
+
+ print(f"\n{'='*60}")
+ print(f"评教完成! 成功: {success}, 失败: {fail}")
+
+
+def parse_cookies_from_file(filepath: str) -> dict:
+ cookies = {}
+ with open(filepath, "r", encoding="utf-8") as f:
+ content = f.read()
+
+ match = re.search(r"Cookie:\s*(.+)", content)
+ if match:
+ cookie_str = match.group(1)
+ for part in cookie_str.split(";"):
+ part = part.strip()
+ if "=" in part:
+ key, value = part.split("=", 1)
+ cookies[key.strip()] = value.strip()
+ return cookies
+
+
+def main():
+ import argparse
+ parser = argparse.ArgumentParser(description="超星学习通自动评教脚本")
+ parser.add_argument("--cookie-file", "-c", type=str,
+ help="Cookie文件路径(从抓包数据中复制的包含Cookie的请求头文件)")
+ parser.add_argument("--cookie-string", "-s", type=str,
+ help="Cookie字符串,例如: `fid=356; _uid=123456; ...`")
+ parser.add_argument("--username", "-u", type=str,
+ help="手机号/学号(用于自动登录)")
+ parser.add_argument("--password", "-p", type=str,
+ help="密码(用于自动登录)")
+ parser.add_argument("--use-config", action="store_true",
+ help="使用config.py中的配置(自动管理Cookie)")
+ parser.add_argument("--questionnaire-id", "-q", type=int, default=None,
+ help="问卷ID,不传则自动发现")
+ parser.add_argument("--fid", type=int, default=None,
+ help="学校fid,默认从Cookie中自动获取")
+ parser.add_argument("--uid", type=int, default=None,
+ help="用户ID,默认从Cookie中自动获取")
+ parser.add_argument("--list", "-l", action="store_true",
+ help="仅列出可用问卷,不执行评教")
+ parser.add_argument("--no-save-cookies", action="store_true",
+ help="不保存Cookie到config.json")
+ parser.add_argument("--qr", action="store_true",
+ help="使用二维码登录")
+
+ args = parser.parse_args()
+
+ if args.qr:
+ evaluator = ChaoxingEvaluator.qr_login()
+ elif args.use_config:
+ evaluator = ChaoxingEvaluator.auto()
+ elif args.username and args.password:
+ evaluator = ChaoxingEvaluator.login(args.username, args.password)
+ if not args.no_save_cookies:
+ cfg.save_cookies(evaluator.get_cookies_dict())
+ print("Cookie已保存到 config.json")
+ elif args.cookie_file:
+ cookies = parse_cookies_from_file(args.cookie_file)
+ evaluator = ChaoxingEvaluator(cookies)
+ elif args.cookie_string:
+ cookies = {}
+ for part in args.cookie_string.split(";"):
+ part = part.strip()
+ if "=" in part:
+ key, value = part.split("=", 1)
+ cookies[key.strip()] = value.strip()
+ evaluator = ChaoxingEvaluator(cookies)
+ else:
+ parser.print_help()
+ print("\n错误: 请使用 --qr 或 --use-config 或提供账号密码或Cookie")
+ return
+
+ if args.list:
+ qs = evaluator.discover_questionnaires()
+ if not qs:
+ print("未发现可用问卷")
+ return
+ print("可用问卷:")
+ for q in qs:
+ print(f" ID: {q['id']} [{q['name']}]")
+ return
+
+ questionnaire_id = args.questionnaire_id
+ if not questionnaire_id:
+ qs = evaluator.discover_questionnaires()
+ if not qs:
+ print("错误: 无法自动发现问卷ID,请通过 --questionnaire-id 手动指定")
+ return
+ if len(qs) == 1:
+ questionnaire_id = qs[0]["id"]
+ print(f"自动发现问卷: ID={questionnaire_id} [{qs[0]['name']}]")
+ else:
+ print("发现多个可用问卷:")
+ for i, q in enumerate(qs, 1):
+ print(f" [{i}] ID: {q['id']} [{q['name']}]")
+ try:
+ choice = int(input("请选择序号: ").strip())
+ questionnaire_id = qs[choice - 1]["id"]
+ except (ValueError, IndexError):
+ print("无效选择")
+ return
+
+ cookies = evaluator.session.cookies
+ uid = args.uid or _get_cookie_int(cookies, "_uid")
+ fid = args.fid or _get_cookie_int(cookies, "fid")
+
+ if not uid:
+ print("错误: 无法获取用户ID,请通过 --uid 参数提供")
+ return
+ if not fid:
+ print("错误: 无法获取fid,请通过 --fid 参数提供")
+ return
+
+ evaluator.batch_evaluate(questionnaire_id, fid=fid, uid=uid)
+
+
+def _get_cookie_int(cookies, name):
+ val = cookies.get(name)
+ if val:
+ try:
+ return int(val)
+ except (ValueError, TypeError):
+ pass
+ return None
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except KeyboardInterrupt:
+ print("\n\n用户中断,已退出")
+ sys.exit(0)
diff --git a/data/config.example.py b/data/config.example.py
new file mode 100644
index 0000000..e01f48e
--- /dev/null
+++ b/data/config.example.py
@@ -0,0 +1,34 @@
+import json
+import os
+import time
+
+CONFIG_FILE = os.path.join(os.path.dirname(__file__), "config.json")
+
+USERNAME = "your_username" # 替换为你的学习通用户名
+PASSWORD = "your_password" # 替换为你的学习通密码
+QR_LOGIN = False # 设为 True 启用二维码登录(优先级高于账号密码)
+
+
+def save_cookies(cookies_dict: dict):
+ data = {
+ "cookies": cookies_dict,
+ "saved_at": int(time.time()),
+ }
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
+ json.dump(data, f, ensure_ascii=False, indent=2)
+
+
+def load_cookies() -> dict | None:
+ if not os.path.exists(CONFIG_FILE):
+ return None
+ try:
+ with open(CONFIG_FILE, "r", encoding="utf-8") as f:
+ data = json.load(f)
+ return data.get("cookies")
+ except (json.JSONDecodeError, IOError):
+ return None
+
+
+def clear_cookies():
+ if os.path.exists(CONFIG_FILE):
+ os.remove(CONFIG_FILE)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..e83338c
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+requests>=2.28.0
+pycryptodome>=3.15.0