Files
chaoxing_evaluate/chaoxing_evaluate.py
EchoZenith 4185141e7e feat: 添加超星学习通自动评教脚本及配置文件
实现超星学习通自动评教功能,支持账号密码登录和二维码登录
添加配置文件模板、依赖文件及gitignore配置
2026-05-04 23:07:19 +08:00

677 lines
26 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'<input[^>]*name=["\']groupTargetIds["\'][^>]*value=["\'](\d+)["\']'
)
type_pattern = re.compile(
r'<input[^>]*name=["\'](\d+)_type["\'][^>]*value=["\'](\d+)["\']'
)
maxscore_pattern = re.compile(
r'<input[^>]*\bid=["\'](\d+)["\'][^>]*\bmaxScore=["\']([\d.]+)["\']'
)
minscore_pattern = re.compile(
r'<input[^>]*\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)