From d78e9902f2b44b0fd1f2234800ceab6d803542b1 Mon Sep 17 00:00:00 2001 From: flt6 <1404262047@qq.com> Date: Tue, 8 Jul 2025 23:59:54 +0800 Subject: [PATCH] split code into URP class --- .gitignore | 3 +- main.py | 249 +++++++++-------------------------------------------- utils.py | 208 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 249 insertions(+), 211 deletions(-) create mode 100644 utils.py diff --git a/.gitignore b/.gitignore index 5184213..ef828c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .env logs -lessons* \ No newline at end of file +lessons* +__pycache__ \ No newline at end of file diff --git a/main.py b/main.py index 7e9d6d3..3251a5b 100644 --- a/main.py +++ b/main.py @@ -1,199 +1,30 @@ -import base64 -import hashlib -import json -import logging + + +from utils import LessonsException, ReloginException +from utils import sc_send,URP,logger + import re from collections import deque -from datetime import datetime + from os import environ from pathlib import Path from time import sleep, time from typing import Callable, List, Optional -import dotenv import pandas as pd -import requests -from serverchan_sdk import sc_send as _sc_send - -# 配置日志 -logging.basicConfig( - level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" -) -logger = logging.getLogger(__name__) - -Path("logs").mkdir(exist_ok=True) # 确保日志目录存在 -now = datetime.now().strftime("%Y%m%d_%H%M%S") -file = logging.FileHandler(f"logs/lessons_{now}.log", mode="w", encoding="utf-8") -file.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")) -file.setLevel(logging.INFO) -logger.addHandler(file) # 日志输出到文件 -class LessonsException(Exception): - """自定义异常类""" - pass -class ReloginException(LessonsException): - """用于处理需要重新登录的异常""" - pass - - -def sc_send(title: str, desp: str): - if not environ.get("SC_KEY"): - logger.error("SC_KEY 未设置,无法发送通知") - return - sc_key = environ.get("SC_KEY") - try: - _sc_send(sc_key, title, desp, options={"tags": "自动选课"}) - except Exception as e: - logger.error(f"发送失败: {e}") - - -class Lessons: - - def __init__(self,skip_check:bool =False): - self.session = requests.session() +class Lessons(URP): + def __init__(self): + super().__init__() + + self.env_check() + self.term: Optional[str] = None - self.fajhh: Optional[str] = None - - # 加载环境变量 - dotenv.load_dotenv() - - # 检查必需的环境变量 - required_keys = [ - "uname", - "password", - "recap_username", - "recap_password", - "FILE", - ] - if not skip_check: - for key in required_keys: - if not environ.get(key): - raise LessonsException(f"请在环境变量中设置{key}") - - self.base = environ.get("base", "http://jwstudent.lnu.edu.cn") + self.fajhh: Optional[str] = None self.interval_1 = int(environ.get("INTERVAL_1", 2)) # 请求间隔,默认为2秒 self.interval_2 = int(environ.get("INTERVAL_2", 10)) # 请求间隔,默认为10秒 - - def _retry_request( - self, func, max_retries: int = 10, error_msg: str = "请求失败" - ) -> requests.Response: - """通用的请求重试方法""" - for attempt in range(1, max_retries + 1): - try: - response = func() - self.judge_logout(response) - return response - except ( - requests.ConnectionError, - requests.HTTPError, - requests.Timeout, - ) as e: - logger.warning(f"{error_msg}!{type(e).__name__}: {e}") - if attempt < max_retries: - logger.info(f"第{attempt}次重试") - else: - raise LessonsException(f"{error_msg}!请检查网络连接!") - - # 这行不会被执行,但为了类型检查 - raise LessonsException(f"{error_msg}!重试次数耗尽") - - @staticmethod - def recapture(b64: str) -> str: - """验证码识别""" - recap_username = environ.get("recap_username") - recap_password = environ.get("recap_password") - - if not recap_username or not recap_password: - raise LessonsException("验证码识别服务配置不完整") - - data = { - "username": recap_username, - "password": recap_password, - "ID": "04897896", - "b64": b64, - "version": "3.1.1", - } - - try: - response = requests.post( - "http://www.fdyscloud.com.cn/tuling/predict", json=data, timeout=10 - ) - response.raise_for_status() - result = response.json() - return result["data"]["result"] - except (requests.RequestException, KeyError, json.JSONDecodeError) as e: - raise LessonsException(f"验证码识别失败: {e}") - - @staticmethod - def pwd_md5(string: str) -> str: - md5_part1 = hashlib.md5((string + "{Urp602019}").encode()).hexdigest().lower() - md5_part2 = hashlib.md5(string.encode()).hexdigest().lower() - final_result = md5_part1 + "*" + md5_part2 - return final_result - - def _login(self): - """登录模块""" - username = environ.get("uname") - password = environ.get("password") - - if not username or not password: - raise LessonsException("用户名或密码未设置") - - try: - # 获取登录页面的token - req = self.session.get("http://jwstudent.lnu.edu.cn/login") - req.raise_for_status() - html = req.text - match = re.search(r'name="tokenValue" value="(.+?)">', html) - if not match: - raise LessonsException("未找到 tokenValue") - token_value = match.group(1) - - # 获取验证码 - req = self.session.get(f"{self.base}/img/captcha.jpg") - req.raise_for_status() - im = req.content - b64 = base64.b64encode(im).decode("utf-8") - captcha_code = self.recapture(b64=b64) - - logger.info(f"验证码识别结果: {captcha_code}") - - hashed_password = self.pwd_md5(password) - - # 模拟请求的 payload - payload = { - "j_username": username, - "j_password": hashed_password, - "j_captcha": captcha_code, - "tokenValue": token_value, - } - - # 发送 POST 请求 - url = f"{self.base}/j_spring_security_check" - headers = { - "User-Agent": "Mozilla/5.0", - "Content-Type": "application/x-www-form-urlencoded", - } - response = self.session.post(url, data=payload, headers=headers) - - if "发生错误" in response.text: - err = re.search(r"发生错误!(.+)", response.text) - if err: - error_message = err.group(1).strip() - raise LessonsException(f"登录失败: {error_message}") - raise LessonsException("登录失败") - - logger.info("登录成功") - return True - - except requests.RequestException as e: - raise LessonsException(f"登录过程中网络错误: {e}") - - def judge_logout(self, response: requests.Response): - """检查账号是否在其他地方被登录""" - if response.url == f"{self.base}/login?errorCode=concurrentSessionExpired": - raise ReloginException("有人登录了您的账号!") + def get_base_info(self): res = self.session.get(f"{self.base}/student/courseSelect/gotoSelect/index") @@ -389,19 +220,7 @@ class Lessons: logger.warning(f"选课 {cl[2]} 结果查询超时,可能未成功选课") return False - def login(self): - logger.info("尝试登录") - flag = False - for i in range(10): - try: - if self._login(): - flag = True - break - except Exception as e: - logger.error(f"登录失败: {e}") - if not flag: - raise LessonsException("登录失败,无法获取token") - + def auto_spider(self): """自动选课主程序""" try: @@ -517,23 +336,31 @@ class Lessons: sc_send("选课异常", desp=f"选课过程中发生意外错误: {e}") raise e -class Grade: +class Grade(URP): def __init__(self): + super().__init__() self.total = None - self.lesson = Lessons(skip_check=False) + required_keys = [ + "uname", + "password", + "recap_username", + "recap_password", + ] + self.env_check(required_keys) + self.interval_2 = int(environ.get("interval_2",3600)) def query(self) -> tuple[dict[str,dict[str,str]],set[str]]: - url = f"{self.lesson.base}/student/integratedQuery/scoreQuery/thisTermScores/index" - res = self.lesson._retry_request(lambda:self.lesson.session.get(url)) + url = f"{self.base}/student/integratedQuery/scoreQuery/thisTermScores/index" + res = self._retry_request(lambda:self.session.get(url)) res.raise_for_status() html = res.text match = re.search(f"/student/integratedQuery/scoreQuery/.+/thisTermScores/data",html) if match: - url = self.lesson.base+match.group(0) + url = self.base+match.group(0) else: raise RuntimeError("Cannot find url") - # url = f"{self.lesson.base}/student/integratedQuery/scoreQuery/U6I5OXib09/thisTermScores/data" - res = self.lesson._retry_request(lambda :self.lesson.session.get(url)) + # url = f"{self.base}/student/integratedQuery/scoreQuery/U6I5OXib09/thisTermScores/data" + res = self._retry_request(lambda :self.session.get(url)) # print(res.text) res_json = res.json() l = res_json[0]["list"] @@ -549,8 +376,8 @@ class Grade: return f"|{x['courseName']}|{x['courseScore']}|{x['maxcj']}|{x['avgcj']}|" def auto_check(self): - self.lesson.login() - # self.lesson.session.cookies.update({"student.urpSoft.cn":"aaapnXQb62LApgwx7lkFz","UqZBpD3n3iXPAw1X9DmYiUaISMkd8YhMUen0":"v1IraGSUs3hnH"}) + self.login() + # self.session.cookies.update({"student.urpSoft.cn":"aaapnXQb62LApgwx7lkFz","UqZBpD3n3iXPAw1X9DmYiUaISMkd8YhMUen0":"v1IraGSUs3hnH"}) grades = set() self.query() @@ -562,6 +389,7 @@ class Grade: while len(grades) < self.total: try: + logger.info("Querying") cls, new = self.query() cls:dict[str,dict[str,str]] new:set[str] @@ -596,18 +424,19 @@ class Grade: except ReloginException as e: logger.info("Relogin") sc_send("成绩监控","重新登录") - self.lesson.login() + self.login() except Exception as e: logger.error(f"Failed to update due to {e}") err+=1 if err >= 5: logger.error("Try to relogin") sc_send("成绩监控","多次失败,尝试重新登录") - self.lesson.login() - + self.login() - sleep(self.lesson.interval_2) - + logger.info(f"Next query will start after {self.interval_2}s") + sleep(self.interval_2) + logger.info("Normal terminated due to all grades is out.") + sc_send("成绩监控","所有成绩均已公布") diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..837faa6 --- /dev/null +++ b/utils.py @@ -0,0 +1,208 @@ +from serverchan_sdk import sc_send as _sc_send +from datetime import datetime +import logging +from pathlib import Path +from os import environ +from dotenv import load_dotenv +import requests +import base64 +import hashlib +import json +import re +from typing import NoReturn,Optional,Iterable + +class LessonsException(Exception): + """自定义异常类""" + pass +class ReloginException(LessonsException): + """用于处理需要重新登录的异常""" + pass + +# 配置日志 +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +def sc_send(title: str, desp: str): + if not environ.get("SC_KEY"): + logger.error("SC_KEY 未设置,无法发送通知") + return + sc_key = environ.get("SC_KEY") + try: + _sc_send(sc_key, title, desp, options={"tags": "自动选课"}) + except Exception as e: + logger.error(f"发送失败: {e}") + + +def log_init(): + Path("logs").mkdir(exist_ok=True) # 确保日志目录存在 + now = datetime.now().strftime("%Y%m%d_%H%M%S") + file = logging.FileHandler(f"logs/lessons_{now}.log", mode="w", encoding="utf-8") + file.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")) + file.setLevel(logging.INFO) + logger.addHandler(file) # 日志输出到文件 + +class URP: + def __init__(self,base=None): + self.session = requests.session() + if base is None: + self.base = environ.get("base","http://jwstudent.lnu.edu.cn") + else: + self.base = base + + @staticmethod + def env_check(required_keys:Optional[Iterable] = None) -> None|NoReturn: + # 检查必需的环境变量 + if required_keys is None: + required_keys = [ + "uname", + "password", + "recap_username", + "recap_password", + "FILE", + ] + for key in required_keys: + if not environ.get(key): + raise LessonsException(f"请在环境变量中设置{key}") + + @staticmethod + def _retry_request( + func, max_retries: int = 10, error_msg: str = "请求失败" + ) -> requests.Response: + """通用的请求重试方法""" + for attempt in range(1, max_retries + 1): + try: + response = func() + URP._judge_logout(response) + return response + except ( + requests.ConnectionError, + requests.HTTPError, + requests.Timeout, + ) as e: + logger.warning(f"{error_msg}!{type(e).__name__}: {e}") + if attempt < max_retries: + logger.info(f"第{attempt}次重试") + else: + raise LessonsException(f"{error_msg}!请检查网络连接!") + + # 这行不会被执行,但为了类型检查 + raise LessonsException(f"{error_msg}!重试次数耗尽") + + @staticmethod + def recapture(b64: str) -> str: + """验证码识别""" + recap_username = environ.get("recap_username") + recap_password = environ.get("recap_password") + + if not recap_username or not recap_password: + raise LessonsException("验证码识别服务配置不完整") + + data = { + "username": recap_username, + "password": recap_password, + "ID": "04897896", + "b64": b64, + "version": "3.1.1", + } + + try: + response = requests.post( + "http://www.fdyscloud.com.cn/tuling/predict", json=data, timeout=10 + ) + response.raise_for_status() + result = response.json() + return result["data"]["result"] + except (requests.RequestException, KeyError, json.JSONDecodeError) as e: + raise LessonsException(f"验证码识别失败: {e}") + + @staticmethod + def pwd_md5(string: str) -> str: + md5_part1 = hashlib.md5((string + "{Urp602019}").encode()).hexdigest().lower() + md5_part2 = hashlib.md5(string.encode()).hexdigest().lower() + final_result = md5_part1 + "*" + md5_part2 + return final_result + + def _login(self): + """登录模块""" + username = environ.get("uname") + password = environ.get("password") + + if not username or not password: + raise LessonsException("用户名或密码未设置") + + try: + # 获取登录页面的token + req = self.session.get("http://jwstudent.lnu.edu.cn/login") + req.raise_for_status() + html = req.text + match = re.search(r'name="tokenValue" value="(.+?)">', html) + if not match: + raise LessonsException("未找到 tokenValue") + token_value = match.group(1) + + # 获取验证码 + req = self.session.get(f"{self.base}/img/captcha.jpg") + req.raise_for_status() + im = req.content + b64 = base64.b64encode(im).decode("utf-8") + captcha_code = self.recapture(b64=b64) + + logger.info(f"验证码识别结果: {captcha_code}") + + hashed_password = self.pwd_md5(password) + + # 模拟请求的 payload + payload = { + "j_username": username, + "j_password": hashed_password, + "j_captcha": captcha_code, + "tokenValue": token_value, + } + + # 发送 POST 请求 + url = f"{self.base}/j_spring_security_check" + headers = { + "User-Agent": "Mozilla/5.0", + "Content-Type": "application/x-www-form-urlencoded", + } + response = self.session.post(url, data=payload, headers=headers) + + if "发生错误" in response.text: + err = re.search(r"发生错误!(.+)", response.text) + if err: + error_message = err.group(1).strip() + raise LessonsException(f"登录失败: {error_message}") + raise LessonsException("登录失败") + + logger.info("登录成功") + return True + + except requests.RequestException as e: + raise LessonsException(f"登录过程中网络错误: {e}") + + def login(self): + logger.info("尝试登录") + flag = False + for i in range(10): + try: + if self._login(): + flag = True + break + except Exception as e: + logger.error(f"登录失败: {e}") + if not flag: + raise LessonsException("登录失败,无法获取token") + + + @staticmethod + def _judge_logout(response: requests.Response): + """检查账号是否在其他地方被登录""" + if "errorCode=concurrentSessionExpired" in response.url: + raise ReloginException("有人登录了您的账号!") + + + +load_dotenv(override=True) +log_init() \ No newline at end of file