from dataclasses import dataclass, asdict from enum import Enum, auto import requests from lxml import etree class Term(Enum): SPRING = auto() AUTUMN = auto() ALL = auto() def __repr__(self): return self.name.lower() class GLUTAcademic: class LoginFailedError(Exception): pass FINAL_GRADE_DICT = { '优秀': 95, '良': 85, '中': 75, '及格': 65, '不及格': 40, '': 0, } @dataclass class Score: year: int term: Term department: str course_number: int course_name: str course_sequence: int instructor: str final_grade: int gpa: float credits: float hours: float assessment_method: str course_attribute: str notes: str exam_type: str retake_flag: str course_requirements: str course_category: str coefficient: str second_degree_minor: str pass_flag: str def as_dict(self): d = asdict(self) d['term'] = repr(self.term) return d def __str__(self): return (f"Score(学年={self.year}, 学期={self.term}, 开课院系={self.department}, " f"课程号={self.course_number}, 课程名={self.course_name}, 课序号={self.course_sequence}, " f"主讲教师={self.instructor}, 总评={self.final_grade}, 绩点={self.gpa}, " f"学分={self.credits}, 学时={self.hours}, 考核方式={self.assessment_method}, " f"选课属性={self.course_attribute}, 备注={self.notes}, 考试性质={self.exam_type}, " f"是否缓考={self.retake_flag}, 课程要求={self.course_requirements}, " f"课程类别={self.course_category}, 系数={self.coefficient}, " f"二学位辅修={self.second_degree_minor}, 及格标志={self.pass_flag})") def __init__(self, username: int, password: str): self.session = requests.Session() self.username = username self.password = password self.login_status = False self.base_url = 'https://jw.glut.edu.cn' def login(self): url = f"{self.base_url}/academic/j_acegi_security_check"\ f"?j_username={self.username}&j_password={self.password}&j_captcha=undefined" res = self.session.get(url) if res.status_code == 200 and res.headers.get('X-Frame-Options') != 'DENY': self.login_status = True else: raise self.LoginFailedError('login error maybe username or password is wrong') def get_cookies(self): if not self.login_status: return None return '; '.join([f'{key}={value}' for key, value in self.session.cookies.items()]) def get_scores_raw(self, year: int = 2024, term: Term = Term.ALL): url = f"{self.base_url}/academic/manager/score/studentOwnScore.do" headers = {'Content-Type': 'application/x-www-form-urlencoded'} term_dict = { Term.SPRING: "1", Term.AUTUMN: "2", Term.ALL: "" } data = { 'year': year - 2013 + 33, 'term': term_dict[term], 'prop': '', 'groupName': '', 'para': '0', 'sortColumn': '', 'Submit': '查询' } res = self.session.post(url, data=data, headers=headers) if res.status_code != 200 or res.headers.get('X-Frame-Options') == 'DENY': raise RuntimeError("Failed to get scores") return res.content def parse_scores(self, raw_score: bytes) -> list[Score]: tree = etree.fromstring(raw_score, parser=etree.HTMLParser(encoding='utf-8')) table = tree.xpath('//table[@class="datalist"]') if not table or len(table) != 1: err_msg = f"Failed to parse scores {table}" with open('error.html', 'wb') as f: f.write(raw_score) raise RuntimeError(err_msg) table = table[0] tr_list = table.xpath('tr')[1:] return [self.parse_scope(tr.xpath('td/text()')) for tr in tr_list] def parse_scope(self, texts: list[str]) -> Score: texts = [t.strip() for t in texts] final_grade = self.FINAL_GRADE_DICT.get(texts[7], -1) if final_grade == -1: try: final_grade = float(texts[7]) except ValueError as exc: raise ValueError('Final grade is not a int number') from exc term_map = { '春': Term.SPRING, '秋': Term.AUTUMN } term = term_map.get(texts[1]) if term is None: raise ValueError(f'term error maybe 春 or 秋 but got {texts[1]}') return self.Score( year = int(texts[0]), term = term, department= texts[2], course_number = int(texts[3]), course_name = texts[4], course_sequence = int(texts[5]), instructor = texts[6], final_grade = final_grade, gpa = float(texts[8]), credits = float(texts[9]), hours = float(texts[10]), assessment_method = texts[11], course_attribute = texts[12], notes = texts[13], exam_type = texts[14], retake_flag = texts[15], course_requirements = texts[16], course_category = texts[17], coefficient = texts[18], second_degree_minor = texts[19], pass_flag = texts[20] ) def get_scores(self, year: int = 2024, term: Term = Term.ALL) -> list[Score]: raw_score = self.get_scores_raw(year, term) return self.parse_scores(raw_score) def calculate_scores(self, scores: list[Score]) -> float: if not scores: return 0.0 total_credits = 0 total_grades = 0 for i in scores: if i.course_attribute != '必修' or i.course_name.startswith('体育'): continue total_credits += i.credits total_grades += i.final_grade * i.credits return total_grades / total_credits