前言
因为我家羽羽说她的课表一天一个样,每次都手动输入到日历中的话非常不方便,所以我打算帮她把课表从教务网站上爬出来,然后写在ics文件中,这样就可以一键导入了。
方案
打开他们学校的教务网站,我就麻了,因为必须要输入验证码。这对自动化脚本来说是一个非常阴间的事情,所以考虑使用selenium
,打开网页后,人工登录,程序检测到网页跳转后开始运行。
登录完成后就可以看到课表,默认是月课表,所以只需要让脚本自己翻页然后读取网页全部内容即可。通过F12
,我们可以看到那些写着有课程信息的元素大致长什么样子,使用正则表达式即可爬取所有课程了。
使用
在经历了漫长了写代码过程之后,终于到了运行的时候啦!看到一个程序能跑起来,真的是很开心的一件事!(大家也可以尝试复制下面的代码
,自己运行试试看)
如果读者想要运行以下代码,需要进行下列步骤:
- 安装一个Python(这不是废话吗,建议是3.6以上,我自己用的3.10)
- 安装
icalendar
和selenium
(在命令行执行pip install xxx
) - 如果你使用
Chrome
浏览器,请下载和你的Chrome
匹配的chromedriver
(见参考链接6),并为chromedriver
添加环境变量,或放在脚本所在目录下。 - 如果你不使用
Chrome
浏览器,请自行搜索selenium
如何使用你的浏览器(FireFox
,Edge
和Safari
等主流浏览器都是支持的,如果你使用其他浏览器,也可以尝试,因为它们大概率和Google Chrome
使用同样的内核,只不过版本较老)另外,不要忘记更改chrome = Chrome()
这条语句。 - 在脚本所在目录下打开命令行,输入
python xxx
即可运行(xxx
是你的脚本名,通常你应该让后缀是.py
)(通常安装python时会关联文件,所以双击运行也不是不可以) - 日程会储存在
cdutcm.ics
中,许多日历App都支持.ics
格式
配置更改
有如下内容可能需要更改:
- 如果你不是使用的
Chrome
,更改chrome = Chrome()
(第68行) - 默认只会获取课程,而不会获取考试,若要获取考试请更改正则表达式中的
上课任务
(第32行) - 默认是秋季学期课表,若要获取春季学期课表,更改
while month != '八月'
和while month != '二月'
,交换八月
金和二月
的位置大概就可以了(第76和83行) - 更改
begin_date = datetime(2022, 8, 29, tzinfo=UTC8)
中的2022, 8, 29
为本学期开学第一周星期一的时间(第97行)
代码
# coding=utf-8
import re
from time import sleep
from typing import Any
import icalendar as ics
from selenium.webdriver import Chrome
from selenium.webdriver.common.by import By
from datetime import datetime, timezone, timedelta
UTC8 = timezone(timedelta(hours=8))
def create_event(name: Any, location: Any, dtstart: Any, dtend: Any, description: Any) -> ics.Event:
'''
create a single icalendar event
:type of params: any Python native type or icalendar property type.
'''
event = ics.Event()
event.add('summary', name)
event.add('location', location)
event.add('dtstart', dtstart)
event.add('dtend', dtend)
event.add('description', description)
return event
def get_lesson_from_html(begin_date: datetime, html: str, begin_week: int = -1) -> tuple[list[dict[str,str|datetime]],int]:
REGEX = (
r'<a class="fc-day-grid-event fc-h-event fc-event fc-start fc-end" lay-tips="'
r'<table class="kb-tips" border="1">'
r'<tr><th>事件类型:</th><td colspan="3">上课任务</td></tr><tr><th>'
r'上课时间:</th><td colspan="3">(.*?)--(.*?)</td></tr><tr><th>'
r'教学模式:</th><td colspan="3">.*?</td></tr><tr><th>'
r'教学形式:</th><td colspan="3">.*?</td></tr><tr><th>'
r'星期:</th><td>(\d*?)</td><th>节次:</th><td>\d*?</td></tr><tr><th>上课周次:</th><td >(\d*?)</td><th>'
r'课序号:</th><td>.*?</td></tr><tr><th>'
r'课程:</th><td colspan="3">\[.*?\]\[.*?\](.*?)\[.*?\]</td></tr><tr><th>'
r'授课教师:</th><td colspan="3">(.*?)</td></tr><tr><th>'
r'教学场地:</th><td colspan="3">(.*?)\(?\)?</td></tr><tr><th>'
r'上课班级:</th><td colspan="3">.*?</td></tr><tr><th>'
r'排课/上课:</th><td colspan="3">.*?</td></tr><tr><th>'
r'授课内容:</th><td colspan="3">((.|\n)*?)</td></tr></table>" style="background-color: rgb\(\d+, \d+, \d+\);">'
)
end_week = 0
lessons : list[dict[str,str|datetime]] = []
for match in re.findall(REGEX, html):
day = int(match[2]) - 1
week = int(match[3]) - 1
if week <= begin_week: continue
end_week = max(end_week, week)
today = begin_date + timedelta(days=7 * week + day)
start_t = [int(x) for x in match[0].split(':')]
end_t = [int(x) for x in match[1].split(':')]
dtstart = today + timedelta(hours=start_t[0], minutes=start_t[1], seconds=start_t[2])
dtend = today + timedelta(hours=end_t[0], minutes=end_t[1], seconds=end_t[2])
name = str(match[4])
teacher = str(match[5])
location = str(match[6])
content = str(match[7])
lessons.append({'课程':name, '教学场地':location, '开始时间':dtstart, '结束时间':dtend, '教师':teacher, '授课内容':content})
return (lessons, end_week)
def get_lesson(begin_date: datetime) -> list[dict[str,str|datetime]]:
# 第一步获取网页元素
chrome = Chrome()
chrome.get('http://jwweb.cdutcm.edu.cn')
while chrome.current_url != 'https://jwweb.cdutcm.edu.cn/new/welcome.page':
sleep(1) # 这里需要手动输入账号密码验证码!
sleep(3) # 等待加载
iframe = chrome.find_element(By.XPATH, '/html/body/div[3]/div/div/div[2]/div/div/iframe')
chrome.switch_to.frame(iframe)
month = chrome.find_element(By.XPATH, '/html/body/div[1]/div/div[1]/div/div/div/div[1]/div[3]/h2').get_attribute('textContent').split(' ')[1]
while month != '八月':
chrome.find_element(By.XPATH, '/html/body/div[1]/div/div[1]/div/div/div/div[1]/div[1]/div/button[1]').click()
sleep(3) # 等待加载
month = chrome.find_element(By.XPATH, '/html/body/div[1]/div/div[1]/div/div/div/div[1]/div[3]/h2').get_attribute('textContent').split(' ')[1]
begin_week = -1
lessons : list[dict[str,str|datetime]] = []
while month != '二月':
html = chrome.page_source
lessons_gets, begin_week = get_lesson_from_html(begin_date, html, begin_week)
lessons += lessons_gets
chrome.find_element(By.XPATH, '/html/body/div[1]/div/div[1]/div/div/div/div[1]/div[1]/div/button[2]').click()
sleep(3) # 等待加载
month = chrome.find_element(By.XPATH, '/html/body/div[1]/div/div[1]/div/div/div/div[1]/div[3]/h2').get_attribute('textContent').split(' ')[1]
return lessons
if __name__ == '__main__':
calendar = ics.Calendar()
calendar.add('version', '2.0')
begin_date = datetime(2022, 8, 29, tzinfo=UTC8)
lessons = get_lesson(begin_date)
for lesson in lessons:
name = lesson['课程']
location = lesson['教学场地']
dtstart = lesson['开始时间']
dtend = lesson['结束时间']
description = '教师:' + str(lesson['教师']) + '\n\n授课内容:\n' + str(lesson['授课内容'])
event = create_event(name, location, dtstart, dtend, description)
calendar.add_component(event)
with open('cdutcm.ics', 'wb') as f:
f.write(calendar.to_ical())