0%

课表助手

简要介绍

作者是一名学生,需要在手机上记录课表,虽然现在已经有很多的课程表App了,但是有些呢不支持从教务系统导入,有些呢会有很多广告,再有些就是界面很丑啦~

因此,为了能够便捷的查看课表,免去现有的课程表软件带来的烦恼,我决定!开发一款无害的课表软件。于是,这款“课表助手”就应运而生(虽然不是一个App,后面再详细解释),它的作用不是显示课表,而是将教务系统中的课表转为iCalendar文件。iCalendar是一种日历数据交换的标准,既然是标准了,那大家肯定都支持啦,目前已经测试了Win10/Android9/iPadOS13,都可以导入至日历。导入后就可以愉快的使用系统日历查看课表了,不得不说,系统自带的日历界面清新、使用流畅、没有广告(用过的都说好🙄)。

开发过程

准备工作

为了明确我要做什么,首先打开了教务系统课程表页面,他是这个样子的:
教务系统
看了一下,嗯!挺好的,适合序列化。然后再看一下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数组] + 尾。每一个VENVENTDTSTAMP创建的时间、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日历,还是很不错的嘛😜~
Win10日历
再打开iPadOS日历看看,已经同步了👍~(前提是登录相同的邮箱)
iPadOS日历
就剩Android咯,打开一看发现并没有同步😥,又去搜了一些教程,并没有什么很好的解决方法,只好把ics文件发送至手机然后手动打开了。
Android日历

脚本下载地址

将以上内容做成了油猴助手脚本并上传至了greasyfork,关于油猴助手请点这里,下载脚本请点这里

💫脚本特点:一键保存ics文件,方便快捷

Github项目地址

千山万水总是情,给个Star行不行 Curriculum-to-iCalendar

目前支持的教务系统

  • WHU

总结

第一次写这种油猴助手的脚本,也算是练习了Javascript的应用吧,过程磕磕绊绊的,从有这个想法到实现经历了两天时间,找朋友测试了一下,发现了很多🐛bug……修改再测试,测试再修改,到现在还算是挺完善的,也欢迎大家在Github提Issue。

如果您觉得这篇分享不错,可以打赏作者一包辣条哦~