feat: 添加超星学习通自动评教脚本及配置文件
实现超星学习通自动评教功能,支持账号密码登录和二维码登录 添加配置文件模板、依赖文件及gitignore配置
This commit is contained in:
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@@ -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
|
||||
676
chaoxing_evaluate.py
Normal file
676
chaoxing_evaluate.py
Normal file
@@ -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'<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)
|
||||
34
data/config.example.py
Normal file
34
data/config.example.py
Normal file
@@ -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)
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
requests>=2.28.0
|
||||
pycryptodome>=3.15.0
|
||||
Reference in New Issue
Block a user