677 lines
26 KiB
Python
677 lines
26 KiB
Python
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)
|