feat: 添加超星学习通自动评教脚本及配置文件

实现超星学习通自动评教功能,支持账号密码登录和二维码登录
添加配置文件模板、依赖文件及gitignore配置
This commit is contained in:
EchoZenith
2026-05-04 23:07:19 +08:00
commit 4185141e7e
4 changed files with 742 additions and 0 deletions

30
.gitignore vendored Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1,2 @@
requests>=2.28.0
pycryptodome>=3.15.0