简要介绍
作者是一名学生,需要在手机上记录课表,虽然现在已经有很多的课程表App了,但是有些呢不支持从教务系统导入,有些呢会有很多广告,再有些就是界面很丑啦~
因此,为了能够便捷的查看课表,免去现有的课程表软件带来的烦恼,我决定!开发一款无害的课表软件。于是,这款“课表助手”就应运而生(虽然不是一个App,后面再详细解释),它的作用不是显示课表,而是将教务系统中的课表转为iCalendar文件。iCalendar是一种日历数据交换的标准,既然是标准了,那大家肯定都支持啦,目前已经测试了Win10/Android9/iPadOS13,都可以导入至日历。导入后就可以愉快的使用系统日历查看课表了,不得不说,系统自带的日历界面清新、使用流畅、没有广告(用过的都说好🙄)。
- 脚本安装请查看Greasyfork
- 完整源码请查看Github
开发过程
准备工作
为了明确我要做什么,首先打开了教务系统课程表页面,他是这个样子的:
看了一下,嗯!挺好的,适合序列化。然后再看一下iCalendar文件的样子:
1 | BEGIN:VCALENDAR |
2 | VERSION:2.0 |
3 | PRODID:Curriculum-to-iCalendar |
4 | BEGIN:VEVENT |
5 | DTSTAMP:20200313T032000Z |
6 | UID:0@https://chaiqingao.github.io/ |
7 | SUMMARY:summary |
8 | DTSTART:20200217T000000Z |
9 | DTEND:20200217T013500Z |
10 | RRULE:FREQ=WEEKLY;UNTIL=20200428T013500Z;INTERVAL=1 |
11 | LOCATION:location |
12 | DESCRIPTION:description |
13 | END:VEVENT |
14 | BEGIN:VEVENT |
15 | ...... |
16 | END:VEVENT |
17 | END:VCALENDAR |
不难看出,一个iCalendar文件由三部分组成:头 + [VENVENT数组] + 尾。每一个VENVENT
有DTSTAMP
创建的时间、UID
标识符、SUMMARY
事件名、DTSTART
事件开始时间、DTEND
事件结束时间、RRULE
重复方式(包括FREQ
频率、UNTIL
重复截至时间、INTERVAL
间隔)、LOCATION
地点、DESCRIPTION
详细信息。掌握了这些东西,下面就开始吧!
定位至教务系统中的课程表
查看网页源码,可以发现,教务系统的课程表嵌套在两层iframe(top->page_iframe->iframe0
)中,经过一番努力,定位到了!
1 | var page_iframe = document.getElementById("page_iframe"); |
2 | var iframe0 = page_iframe.contentDocument.getElementById("iframe0"); |
3 | var table = iframe0.contentDocument.getElementsByTagName("table")[0]; |
下面就可以对表格里的内容为所欲为进行操作了。
获取表格中课程名和上课时间非空的行
第一要事!确保这个课是存在的,这个简单一点:
1 | for (let i = 1; i < table.rows.length; i++) { |
2 | var courseName = table.rows[i].cells[1].innerText; |
3 | var teacherName = table.rows[i].cells[5].innerText; |
4 | var timeAddress = table.rows[i].cells[9].innerText; |
5 | if (courseName !== "" && timeAddress !== "") { |
6 | ... |
7 | } |
8 | } |
构建VEVENT
courseName
就作为SUMMARY
吧,下面是对timeAddress
的解析,从中提取出日期时间地址
1 | var toString = function (date) { |
2 | return date.toISOString().split(/-|:|[.]/).slice(0, 4).join("") + "00Z"; |
3 | } |
4 | var events = timeAddress.split(' ').filter(n => n != ""); |
5 | for (let j = 0; j < events.length; j++) { |
6 | //周一:1-11周,每1周;1-2节,3区,附3-401 ==> [一,1-11,1,1-2,3区,附3-401] |
7 | var informations = events[j].split(/周,每|周|节,|:|,|;/).filter(n => n != ""); |
8 | var description = "第" + events[j].split(";")[1] + " " + teacherName; |
9 | var weekDay = weekToNum[informations[0]]; |
10 | var startWeek = parseInt(informations[1].split("-")[0]); |
11 | var endWeek = parseInt(informations[1].split("-")[1]); |
12 | var interval = parseInt(informations[2]); |
13 | var startTime = class_start[parseInt(informations[3].split('-')[0])]; |
14 | var endTime = class_start[parseInt(informations[3].split('-')[1])]; |
15 | var address = [informations[4], informations[5], teacherName].join(" "); |
16 | var startDate = new Date(),endDate=new Date(),untilDate=new Date(); |
17 | startDate.setDate(startDate.getDate() - startDate.getDay() - (currentWeek - startWeek) * 7 + weekDay); |
18 | startDate.setHours(startTime[0], startTime[1], 0, 0); |
19 | endDate.setDate(endDate.getDate() - endDate.getDay() - (currentWeek - startWeek) * 7 + weekDay); |
20 | endDate.setHours(endTime[0],endTime[1]+class_time); |
21 | untilDate.setDate(untilDate.getDate() - untilDate.getDay() + (endWeek - currentWeek) * 7 + weekDay + 1); |
22 | untilDate.setHours(endTime[0],endTime[1]+class_time); |
23 | calendarEvents.push([ |
24 | 'BEGIN:VEVENT', |
25 | 'DTSTAMP:' + toString(new Date()), |
26 | 'UID:' + calendarEvents.length + '@' + 'https://chaiqingao.github.io/', |
27 | 'SUMMARY:' + courseName, |
28 | 'DTSTART:' + toString(startDate), |
29 | 'DTEND:' + toString(endDate), |
30 | 'RRULE:FREQ=WEEKLY;UNTIL=' + toString(untilDate) + ';INTERVAL=' + interval, |
31 | 'LOCATION:' + address, |
32 | 'DESCRIPTION:' + description, |
33 | 'END:VEVENT' |
34 | ].join(SEPARATOR)); |
35 | } |
这里的calendarEvents
是一个事件数组,里面存储了所有的课程,加上头尾就可以保存了😁
保存文件
这里使用了FileSaver.js
用于保存文件
1 | var fileName = year + "学年第" + term + "学期.ics"; |
2 | var calendar = calendar_start + SEPARATOR + calendarEvents.join(SEPARATOR) + calendar_end; |
3 | var blob; |
4 | if (navigator.userAgent.indexOf('MSIE 10') === -1) { // chrome or firefox |
5 | blob = new Blob([calendar]); |
6 | } else { // ie |
7 | var bb = new BlobBuilder(); |
8 | bb.append(calendar); |
9 | blob = bb.getBlob('text/x-vCalendar;charset=' + document.characterSet); |
10 | } |
结果展示
这里将ics文件导入至登录了Outlook邮箱的Win10日历,还是很不错的嘛😜~
再打开iPadOS日历看看,已经同步了👍~(前提是登录相同的邮箱)
就剩Android咯,打开一看发现并没有同步😥,又去搜了一些教程,并没有什么很好的解决方法,只好把ics文件发送至手机然后手动打开了。
脚本下载地址
将以上内容做成了油猴助手脚本并上传至了greasyfork,关于油猴助手请点这里,下载脚本请点这里
💫脚本特点:一键保存ics文件,方便快捷
Github项目地址
千山万水总是情,给个Star行不行 Curriculum-to-iCalendar
目前支持的教务系统
- WHU
总结
第一次写这种油猴助手的脚本,也算是练习了Javascript的应用吧,过程磕磕绊绊的,从有这个想法到实现经历了两天时间,找朋友测试了一下,发现了很多🐛bug……修改再测试,测试再修改,到现在还算是挺完善的,也欢迎大家在Github提Issue。