diff --git a/cqwu/client.py b/cqwu/client.py index fd790b1..638c9ab 100644 --- a/cqwu/client.py +++ b/cqwu/client.py @@ -15,7 +15,7 @@ class Client(Methods): password: str = None, cookie: str = None, cookie_file_path: str = "cookie.txt", - timeout: int = 10, + timeout: int = 60, ): self.username = username self.password = password diff --git a/cqwu/enums/webvpn.py b/cqwu/enums/webvpn.py index 1f378d3..b700ffb 100644 --- a/cqwu/enums/webvpn.py +++ b/cqwu/enums/webvpn.py @@ -8,3 +8,13 @@ class ExamRound(str, Enum): """ 分散考试 """ Concentration = "3" """ 集中考试 """ + + +class ScoreSearchType(str, Enum): + """ 成绩查询类型 """ + All = "1" + """入学以来""" + XUENIAN = "2" + """学年""" + XUEQI = "3" + """学期""" diff --git a/cqwu/errors/webvpn.py b/cqwu/errors/webvpn.py index abb0019..fe1fca5 100644 --- a/cqwu/errors/webvpn.py +++ b/cqwu/errors/webvpn.py @@ -7,3 +7,7 @@ class CQWUWebVPNError(CQWUEhallError): class NoExamData(CQWUWebVPNError): """ 没有检索到对应的考试记录 """ + + +class NoScoreDetailData(CQWUWebVPNError): + """ 没有检索到对应的成绩明细记录 """ diff --git a/cqwu/methods/webvpn/__init__.py b/cqwu/methods/webvpn/__init__.py index a62a236..e05cfee 100644 --- a/cqwu/methods/webvpn/__init__.py +++ b/cqwu/methods/webvpn/__init__.py @@ -3,6 +3,7 @@ from httpx import URL from .get_calendar import GetCalendar from .get_calendar_change import GetCalendarChange from .get_exam_calendar import GetExamCalendar +from .get_score_detail import GetScoreDetail from .get_selected_courses import GetSelectedCourses from .login_jwmis import LoginJwmis from .login_webvpn import LoginWebVPN @@ -12,6 +13,7 @@ class WebVPN( GetCalendar, GetCalendarChange, GetExamCalendar, + GetScoreDetail, GetSelectedCourses, LoginJwmis, LoginWebVPN, diff --git a/cqwu/methods/webvpn/get_score_detail.py b/cqwu/methods/webvpn/get_score_detail.py new file mode 100644 index 0000000..394ed78 --- /dev/null +++ b/cqwu/methods/webvpn/get_score_detail.py @@ -0,0 +1,136 @@ +from typing import Union, List + +from bs4 import BeautifulSoup, Tag + +import cqwu +from cqwu.enums import ScoreSearchType +from cqwu.errors import NoScoreDetailData +from cqwu.types import ScoreDetail, ScoreDetailInfo, ScoreDetailCourse, ScoreDetailTotal + + +class GetScoreDetail: + async def get_score_detail( + self: "cqwu.Client", + search_type: Union[str, ScoreSearchType] = ScoreSearchType.XUEQI, + xue_nian: int = None, + xue_qi: int = None, + use_model: bool = False, + ) -> Union[str, ScoreDetail]: + """ 获取学业成绩 """ + xue_nian = xue_nian or self.xue_nian + xue_qi = xue_qi or self.xue_qi + search_type = ScoreSearchType(search_type) + 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/xscj.stuckcj_data.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 = { + "sjxz": f"sjxz{search_type.value}", + "ysyx": "yxcj", + "zx": "1", + "fx": "1", + "btnExport": "%B5%BC%B3%F6", + "rxnj": str(xue_nian), + "xn": str(xue_nian), + 'xn1': str(xue_nian + 1), + "xq": str(xue_qi), + "ysyxS": "on", + "sjxzS": "on", + "zxC": "on", + "fxC": "on", + "xsjd": "1", + } + jw_html = await self.request.post(jw_url, data=data, headers=headers, timeout=60, follow_redirects=True) + if "没有检索到记录!" in jw_html.text: + raise NoScoreDetailData("没有检索到记录!") + 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) + + +def parse_info(tag: Tag) -> ScoreDetailInfo: + divs = tag.find_all("div") + + def get_text(_tag) -> str: + return (":".join(_tag.text.split(":")[1:])).strip() + + return ScoreDetailInfo( + college=get_text(divs[0]), + major=get_text(divs[1]), + level=get_text(divs[2]), + class_name=get_text(divs[3]), + student_id=get_text(divs[4]), + student_name=get_text(divs[5]), + date=get_text(divs[6]), + ) + + +def parse_course(tag: Tag) -> List[ScoreDetailCourse]: + trs = tag.find_all("tr") + courses = [] + for tr in trs: + tds = tr.find_all("td") + course = ScoreDetailCourse( + id=tds[0].text, + name=tds[1].text, + credit=tds[2].text, + period=tds[3].text, + type=tds[4].text, + nature=tds[5].text, + exam_method=tds[6].text, + score=tds[7].text, + score_point=tds[8].text, + grade_point=tds[9].text, + gp=tds[10].text, + remark=tds[11].text, + ) + courses.append(course) + return courses + + +def parse_total(tag: Tag) -> ScoreDetailTotal: + tr = tag.find_all("tr")[-1] + tds = tr.find_all("td") + return ScoreDetailTotal( + num=tds[1].text, + credit=tds[2].text, + get_credit=tds[3].text, + grade_point=tds[4].text, + gp=tds[5].text, + gpa=tds[6].text, + score_avg=tds[7].text, + weighted_grade_avg=tds[8].text, + ) + + +def parse_html(html: str) -> ScoreDetail: + soup = BeautifulSoup(html, "lxml") + group_div = soup.find("div", {"group": "group"}) + info = parse_info(group_div) + tbody_s = soup.find_all("tbody") + courses = [] + for tbody in tbody_s[:-1]: + courses += parse_course(tbody) + total = parse_total(tbody_s[-1]) + return ScoreDetail(info=info, courses=courses, total=total) diff --git a/cqwu/types/__init__.py b/cqwu/types/__init__.py index 569f229..a2d5139 100644 --- a/cqwu/types/__init__.py +++ b/cqwu/types/__init__.py @@ -4,4 +4,5 @@ from .epay import PayBill, PayBillPage from .exam import AiExam from .pay import PayProject, PayProjectDetail, PayUser from .score import Score +from .score_detail import ScoreDetail, ScoreDetailInfo, ScoreDetailCourse, ScoreDetailTotal from .user import User diff --git a/cqwu/types/score_detail.py b/cqwu/types/score_detail.py new file mode 100644 index 0000000..4441aff --- /dev/null +++ b/cqwu/types/score_detail.py @@ -0,0 +1,75 @@ +from typing import List + +from pydantic import BaseModel + + +class ScoreDetailInfo(BaseModel): + college: str = "" + """院(系)/部""" + major: str = "" + """专业""" + level: str = "" + """培养层次""" + class_name: str = "" + """行政班级""" + student_id: str = "" + """学号""" + student_name: str = "" + """姓名""" + date: str = "" + """打印时间""" + + +class ScoreDetailCourse(BaseModel): + id: int = 0 + """序号""" + name: str = "" + """课程/环节""" + credit: float = 0.0 + """学分""" + period: int = 0 + """总学时""" + type: str = "" + """类别""" + nature: str = "" + """修读性质""" + exam_method: str = "" + """考核方式""" + score: float = 0.0 + """成绩""" + score_point: float = 0.0 + """获得学分""" + grade_point: float = 0.0 + """绩点""" + gp: float = 0.0 + """学分绩点""" + remark: str = "" + """备注""" + + +class ScoreDetailTotal(BaseModel): + num: int = 0 + """修读课程环节数""" + credit: float = 0.0 + """学分""" + get_credit: float = 0.0 + """获得学分""" + grade_point: float = 0.0 + """获得绩点""" + gp: float = 0.0 + """获得学分绩点""" + gpa: float = 0.0 + """平均学分绩点""" + score_avg: float = 0.0 + """平均成绩""" + weighted_grade_avg: float = 0.0 + """加权平均成绩""" + + +class ScoreDetail(BaseModel): + info: ScoreDetailInfo + """基本信息""" + courses: List[ScoreDetailCourse] + """课程信息""" + total: ScoreDetailTotal + """总计""" diff --git a/setup.py b/setup.py index 2faa281..ccaef98 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.16", # 包版本号,便于维护版本 + version="0.0.17", # 包版本号,便于维护版本 author="omg-xtao", # 作者,可以写自己的姓名 author_email="xtao@xtaolink.cn", # 作者联系方式,可写自己的邮箱地址 description="A cqwu ehall client.", # 包的简述