From 99daff156432bd3a213dab742b653fa4115ca4e8 Mon Sep 17 00:00:00 2001 From: xtaodada Date: Thu, 1 Jun 2023 17:03:36 +0800 Subject: [PATCH] :sparkles: Support get exam calendar --- cqwu/client.py | 6 +- cqwu/enums/__init__.py | 1 + cqwu/enums/webvpn.py | 10 +++ cqwu/errors/__init__.py | 1 + cqwu/errors/webvpn.py | 9 +++ cqwu/methods/webvpn/__init__.py | 4 ++ cqwu/methods/webvpn/get_calendar.py | 7 +- cqwu/methods/webvpn/get_calendar_change.py | 7 +- cqwu/methods/webvpn/get_exam_calendar.py | 81 ++++++++++++++++++++++ cqwu/methods/webvpn/login_jwmis.py | 17 +++++ cqwu/types/__init__.py | 4 ++ cqwu/types/exam.py | 36 ++++++++++ requirements.txt | 4 +- setup.py | 2 +- 14 files changed, 172 insertions(+), 17 deletions(-) create mode 100644 cqwu/enums/webvpn.py create mode 100644 cqwu/errors/webvpn.py create mode 100644 cqwu/methods/webvpn/get_exam_calendar.py create mode 100644 cqwu/methods/webvpn/login_jwmis.py create mode 100644 cqwu/types/exam.py diff --git a/cqwu/client.py b/cqwu/client.py index bc7d09d..5817252 100644 --- a/cqwu/client.py +++ b/cqwu/client.py @@ -29,8 +29,10 @@ class Client(Methods): self.loop = asyncio.get_event_loop() self.me: Optional[User] = None self._use_password_login = False - self.xue_nian = 2022 # 学年 - self.xue_qi = 1 # 学期 0 为第一学期, 1 为第二学期 + self.xue_nian = 2022 + """ 学年 """ + self.xue_qi = 1 + """ 学期,0 为第一学期,1 为第二学期 """ @staticmethod def get_input(word: str = "", is_int: bool = False): diff --git a/cqwu/enums/__init__.py b/cqwu/enums/__init__.py index e69de29..27046cb 100644 --- a/cqwu/enums/__init__.py +++ b/cqwu/enums/__init__.py @@ -0,0 +1 @@ +from .webvpn import * diff --git a/cqwu/enums/webvpn.py b/cqwu/enums/webvpn.py new file mode 100644 index 0000000..1f378d3 --- /dev/null +++ b/cqwu/enums/webvpn.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class ExamRound(str, Enum): + Supplementation = "1" + """ 开学补缓考 """ + Scattered = "2" + """ 分散考试 """ + Concentration = "3" + """ 集中考试 """ diff --git a/cqwu/errors/__init__.py b/cqwu/errors/__init__.py index e8cc249..09ab2fe 100644 --- a/cqwu/errors/__init__.py +++ b/cqwu/errors/__init__.py @@ -1,3 +1,4 @@ from .auth import * from .base import * from .epay import * +from .webvpn import * diff --git a/cqwu/errors/webvpn.py b/cqwu/errors/webvpn.py new file mode 100644 index 0000000..abb0019 --- /dev/null +++ b/cqwu/errors/webvpn.py @@ -0,0 +1,9 @@ +from .base import CQWUEhallError + + +class CQWUWebVPNError(CQWUEhallError): + pass + + +class NoExamData(CQWUWebVPNError): + """ 没有检索到对应的考试记录 """ diff --git a/cqwu/methods/webvpn/__init__.py b/cqwu/methods/webvpn/__init__.py index 0c9c088..154eb46 100644 --- a/cqwu/methods/webvpn/__init__.py +++ b/cqwu/methods/webvpn/__init__.py @@ -2,12 +2,16 @@ from httpx import URL from .get_calendar import GetCalendar from .get_calendar_change import GetCalendarChange +from .get_exam_calendar import GetExamCalendar +from .login_jwmis import LoginJwmis from .login_webvpn import LoginWebVPN class WebVPN( GetCalendar, GetCalendarChange, + GetExamCalendar, + LoginJwmis, LoginWebVPN, ): @staticmethod diff --git a/cqwu/methods/webvpn/get_calendar.py b/cqwu/methods/webvpn/get_calendar.py index 44cbd2b..405ae96 100644 --- a/cqwu/methods/webvpn/get_calendar.py +++ b/cqwu/methods/webvpn/get_calendar.py @@ -4,7 +4,6 @@ from typing import Tuple, List, Union from bs4 import BeautifulSoup import cqwu -from cqwu.errors import CookieError from cqwu.types.calendar import AiCourse @@ -18,11 +17,7 @@ class GetCalendar: """ 获取课程表 """ xue_nian = xue_nian or self.xue_nian xue_qi = xue_qi or self.xue_qi - jw_html = await self.request.get( - f"{self.web_ehall_path}/appShow?appId=5299144291521305", follow_redirects=True - ) - if "教学管理服务平台" not in jw_html.text: - raise CookieError + jw_html = await self.login_jwmis() jw_host = self.get_web_vpn_host(jw_html.url) jw_url = f"{jw_host}/cqwljw/student/wsxk.xskcb10319.jsp" params = { diff --git a/cqwu/methods/webvpn/get_calendar_change.py b/cqwu/methods/webvpn/get_calendar_change.py index 69382ba..0f28ed2 100644 --- a/cqwu/methods/webvpn/get_calendar_change.py +++ b/cqwu/methods/webvpn/get_calendar_change.py @@ -1,5 +1,4 @@ import cqwu -from cqwu.errors import CookieError def get_in_middle(text: str, start: str, end: str) -> str: @@ -15,11 +14,7 @@ class GetCalendarChange: """ 获取课程表 """ xue_nian = xue_nian or self.xue_nian xue_qi = xue_qi or self.xue_qi - jw_html = await self.request.get( - f"{self.web_ehall_path}/appShow?appId=5299144291521305", follow_redirects=True - ) - if "教学管理服务平台" not in jw_html.text: - raise CookieError + jw_html = await self.login_jwmis() jw_host = self.get_web_vpn_host(jw_html.url, https=True) jw_url = f"{jw_host}/cqwljw/student/jxap.jxaptzxx_rpt.jsp" jw_sg_url = f"{jw_host}/cqwljw/STU_DynamicInitDataAction.do" diff --git a/cqwu/methods/webvpn/get_exam_calendar.py b/cqwu/methods/webvpn/get_exam_calendar.py new file mode 100644 index 0000000..fcbaefd --- /dev/null +++ b/cqwu/methods/webvpn/get_exam_calendar.py @@ -0,0 +1,81 @@ +from typing import Union, List + +from bs4 import BeautifulSoup + +import cqwu +from cqwu.enums import ExamRound +from cqwu.errors import NoExamData +from cqwu.types import AiExam + + +class GetExamCalendar: + async def get_exam_calendar( + self: "cqwu.Client", + exam_round: Union[str, ExamRound] = ExamRound.Supplementation, + xue_nian: int = None, + xue_qi: int = None, + use_model: bool = False, + ) -> Union[str, List[AiExam]]: + """ 获取考试安排表 """ + xue_nian = xue_nian or self.xue_nian + xue_qi = xue_qi or self.xue_qi + exam_round = ExamRound(exam_round) + jw_html = await self.login_jwmis() + jw_host = self.get_web_vpn_host(jw_html.url, https=True) + jw_url = f"{jw_host}/cqwljw/student/ksap.ksapb_date.jsp" + headers = { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Language': 'zh-CN,zh;q=0.9,zh-Hans;q=0.8,und;q=0.7,en;q=0.6,zh-Hant;q=0.5,ja;q=0.4', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'Content-Type': 'application/x-www-form-urlencoded', + 'DNT': '1', + 'Pragma': 'no-cache', + 'Referer': f'{jw_host}/cqwljw/student/ksap.ksapb.html?menucode=S20403', + 'Sec-Fetch-Dest': 'iframe', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'same-origin', + 'Sec-Fetch-User': '?1', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36', + 'sec-ch-ua': '"Chromium";v="112", "Not:A-Brand";v="99"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"', + } + data = { + 'xn': str(xue_nian), + 'xq': str(xue_qi), + 'title': '', + 'xnxq': f'{xue_nian}{xue_qi}', + 'kslc': exam_round.value, + } + jw_html = await self.request.post(jw_url, data=data, headers=headers, timeout=60, follow_redirects=True) + if "没有检索到记录!" in jw_html.text: + raise NoExamData("没有检索到记录!") + jw_html = jw_html.text.replace("""""", "") + jw_html = jw_html.replace("""""", "") + jw_html = jw_html.replace("charset=GBK", 'charset=UTF-8') + if not use_model: + return jw_html + return parse_html(jw_html, exam_round) + + +def parse_html(html: str, exam_round: ExamRound) -> List[AiExam]: + data: List[AiExam] = [] + soup = BeautifulSoup(html, "html.parser") + trs = soup.find_all("tr")[1:] + for tr in trs: + tds = tr.find_all("td") + if len(tds) != 5: + continue + data.append( + AiExam( + name=tds[0].text, + credit=float(tds[1].text), + time=tds[2].text, + position=tds[3].text, + seat=tds[4].text, + exam_round=exam_round, + ) + ) + return data diff --git a/cqwu/methods/webvpn/login_jwmis.py b/cqwu/methods/webvpn/login_jwmis.py new file mode 100644 index 0000000..3bbfb87 --- /dev/null +++ b/cqwu/methods/webvpn/login_jwmis.py @@ -0,0 +1,17 @@ +from httpx import Response + +import cqwu +from cqwu.errors import CookieError + + +class LoginJwmis: + async def login_jwmis( + self: "cqwu.Client", + ) -> Response: + """ 登录教学管理平台 """ + jw_html = await self.request.get( + f"{self.web_ehall_path}/appShow?appId=5299144291521305", follow_redirects=True + ) + if "教学管理服务平台" not in jw_html.text: + raise CookieError + return jw_html diff --git a/cqwu/types/__init__.py b/cqwu/types/__init__.py index eacbd7e..98260ce 100644 --- a/cqwu/types/__init__.py +++ b/cqwu/types/__init__.py @@ -1,2 +1,6 @@ +from .calendar import AiCourse +from .cp import CP, PublicCPRaw, CPGS +from .epay import PayBill, PayBillPage +from .exam import AiExam from .score import Score from .user import User diff --git a/cqwu/types/exam.py b/cqwu/types/exam.py new file mode 100644 index 0000000..87977ab --- /dev/null +++ b/cqwu/types/exam.py @@ -0,0 +1,36 @@ +import datetime +from typing import Tuple + +from pydantic import BaseModel + +from cqwu.enums import ExamRound + + +class AiExam(BaseModel): + name: str + """ 课程名称 """ + credit: float + """ 学分 """ + time: str + """ 考试时间 """ + position: str + """ 考试地点 """ + seat: str + """ 座位号 """ + exam_round: ExamRound + """ 考试轮次 """ + + @property + def name_no_id(self) -> str: + """ 获取课程名称(去除课程编号) """ + return self.name.split("]")[-1] + + def get_time(self) -> Tuple[datetime.datetime, datetime.datetime]: + """ 获取格式化后的考试时间 """ + # 2023-06-25(18周 星期日)09:00-11:00 + day = datetime.datetime.strptime(self.time.split("(")[0], "%Y-%m-%d") + start_time = datetime.datetime.strptime(self.time.split(")")[1].split("-")[0], "%H:%M") + start_time = datetime.datetime(day.year, day.month, day.day, start_time.hour, start_time.minute) + end_time = datetime.datetime.strptime(self.time.split(")")[1].split("-")[1], "%H:%M") + end_time = datetime.datetime(day.year, day.month, day.day, end_time.hour, end_time.minute) + return start_time, end_time diff --git a/requirements.txt b/requirements.txt index 3254fe3..17e4d3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -httpx==0.23.3 +httpx==0.24.1 lxml==4.9.2 PyExecJS2==1.6.1 -beautifulsoup4==4.11.2 +beautifulsoup4==4.12.2 qrcode==7.4.2 pillow pydantic==1.10.5 diff --git a/setup.py b/setup.py index e4d7d42..c9beae2 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh: setuptools.setup( name="cqwu", # 用自己的名替换其中的YOUR_USERNAME_ - version="0.0.10", # 包版本号,便于维护版本 + version="0.0.11", # 包版本号,便于维护版本 author="omg-xtao", # 作者,可以写自己的姓名 author_email="xtao@xtaolink.cn", # 作者联系方式,可写自己的邮箱地址 description="A cqwu ehall client.", # 包的简述