├── README.md └── ics_gen.py /README.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 3 | 一个从教务自动爬取课表,并转换成 ICS 文件的小工具 4 | 5 | # 使用说明 6 | 7 | 1. 在脚本中注释位置填写学号和密码 8 | 2. 运行脚本 `ics_gen` 9 | 3. 在脚本同目录下会生成一个 `classes.ics`,可以将其导入到日历中(一般双击即可,也可以发送给手机使用) 10 | -------------------------------------------------------------------------------- /ics_gen.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from datetime import datetime 3 | from bs4 import BeautifulSoup 4 | 5 | username = "" # 学号 6 | password = "" # 密码 7 | 8 | BASE_URL = 'http://app.buaa.edu.cn/' 9 | LOGIN_URL = 'https://sso.buaa.edu.cn/login' 10 | 11 | def get_login_token(session): 12 | r = session.get("https://sso.buaa.edu.cn/login?service=https%3A%2F%2Fapp.buaa.edu.cn%2Fa_buaa%2Fapi%2Fcas%2Findex%3Fredirect%3D%252Fsite%252Fcenter%252Fpersonal%26from%3Dwap%26login_from%3D&noAutoRedirect=1") 13 | soup = BeautifulSoup(r.content, 'html.parser') 14 | return soup.find('input', {'name': 'execution'})['value'] 15 | 16 | def login(session): 17 | formdata = { 18 | 'username': username, 19 | 'password': password, 20 | 'execution': get_login_token(session), 21 | 'type': 'username_password', 22 | '_eventId': 'submit', 23 | 'submit': '登录' 24 | } 25 | r = session.post(LOGIN_URL, data=formdata, allow_redirects=False) 26 | location = r.headers.get('Location') 27 | if location: 28 | return location 29 | else: 30 | exit('登录失败,请自行排错') 31 | 32 | def get_eai_sess() -> str: 33 | url = 'https://app.buaa.edu.cn/uc/wap/login' 34 | r = requests.get(url, allow_redirects=False) 35 | cookies = requests.utils.dict_from_cookiejar(r.cookies) 36 | return cookies["eai-sess"] 37 | 38 | def verify_eai_sess(): 39 | session = requests.session() 40 | url = login(session) 41 | eai_sess = get_eai_sess() 42 | 43 | hearders ={ 44 | 'cookie': eai_sess 45 | } 46 | r = requests.get(url,headers=hearders,allow_redirects=False) 47 | cookies = requests.utils.dict_from_cookiejar(r.cookies) 48 | date = r.headers.get('date') 49 | if date: 50 | return cookies["eai-sess"] 51 | else: 52 | exit('以上内容出错,请自行排错') 53 | 54 | def merge_adjacent_classes(classes: list) -> list: 55 | i = 1 56 | while i < len(classes): 57 | if classes[i]["course_name"] == classes[i-1]["course_name"]: 58 | classes[i-1]["lessons"] += f'{classes[i]["lessons"]}' 59 | classes[i-1]["course_time"] = f'{classes[i-1]["course_time"].split("~")[0]}~{classes[i]["course_time"].split("~")[1]}' 60 | classes.pop(i) 61 | else: 62 | i += 1 63 | return classes 64 | 65 | def get_class_by_week(year: str, term: str, week: str, eai_sess: str) -> list[dict]: 66 | class_url = "https://app.buaa.edu.cn/timetable/wap/default/get-datatmp" 67 | header = { 68 | "Cookie": f"eai-sess={eai_sess}", 69 | } 70 | data = { 71 | "year": year, 72 | "term": term, 73 | "week": week, 74 | "type": "1", 75 | } 76 | 77 | r = requests.post(class_url, data=data, headers=header) 78 | days = r.json()["d"]["weekdays"] 79 | classes = r.json()["d"]["classes"] 80 | classes = merge_adjacent_classes(sorted(classes, key=lambda klass: int(klass["weekday"]) * 100 + int(klass["lessons"][0:1]))) 81 | 82 | for klass in classes: 83 | klass["date"] = days[int(klass["weekday"]) - 1].replace("-", "") 84 | klass["start"] = klass["course_time"].split("~")[0].replace(":", "") 85 | klass["end"] = klass["course_time"].split("~")[1].replace(":", "") 86 | klass["lessons"] = ", ".join([klass["lessons"][i:i+2] for i in range(0, len(klass["lessons"]), 2)]) 87 | 88 | return classes 89 | 90 | def generate_ics(title: str, classes: list) -> str: 91 | ics_payload = f"""BEGIN:VCALENDAR 92 | VERSION:2.0 93 | X-WR-CALNAME:{title} 94 | CALSCALE:GREGORIAN 95 | BEGIN:VTIMEZONE 96 | TZID:Asia/Shanghai 97 | TZURL:http://tzurl.org/zoneinfo-outlook/Asia/Shanghai 98 | X-LIC-LOCATION:Asia/Shanghai 99 | BEGIN:STANDARD 100 | TZOFFSETFROM:+0800 101 | TZOFFSETTO:+0800 102 | TZNAME:CST 103 | DTSTART:19700101T000000 104 | END:STANDARD 105 | END:VTIMEZONE""" 106 | 107 | for klass in classes: 108 | event_start = f'{klass["date"]}T{klass["start"].rjust(4, "0")}00' 109 | event_end = f'{klass["date"]}T{klass["end"].rjust(4, "0")}00' 110 | event_description = f"""编号:{klass["course_id"]} 111 | 名称:{klass["course_name"]} 112 | 教师:{klass["teacher"]} 113 | 学分:{klass["credit"]} 114 | 类型:{klass["course_type"]} 115 | 课时:{klass["course_hour"]} 116 | 上课时间:{klass["course_time"]};第 {klass["lessons"]} 节""".replace("\n", "\\n") 117 | event_info = f""" 118 | BEGIN:VEVENT 119 | DESCRIPTION:{event_description} 120 | DTSTART;TZID=Asia/Shanghai:{event_start} 121 | DTEND;TZID=Asia/Shanghai:{event_end} 122 | LOCATION:{klass["location"]} 123 | SUMMARY:{klass["course_name"]} 124 | BEGIN:VALARM 125 | TRIGGER:-PT30M 126 | REPEAT:1 127 | DURATION:PT1M 128 | END:VALARM 129 | END:VEVENT""" 130 | ics_payload += event_info 131 | 132 | ics_payload += "\nEND:VCALENDAR" 133 | return ics_payload 134 | 135 | if __name__ == "__main__": 136 | year = datetime.now().year 137 | month = datetime.now().month 138 | year_str = f"{year-1}-{year}" if month < 7 else f"{year}-{year+1}" 139 | term_str = "2" if month < 7 else "1" 140 | eai_sess = verify_eai_sess() 141 | 142 | weekly_classes = [] 143 | for week in range(1, 20): 144 | week_str = str(week) 145 | weekly_classes.extend(get_class_by_week(year_str, term_str, week_str, eai_sess)) 146 | 147 | with open("classes.ics", "w+", encoding="utf-8") as f: 148 | f.write(generate_ics(f"北航 {year_str} 第 {term_str} 学期课程表", weekly_classes)) --------------------------------------------------------------------------------