小爱课程表导入课程开发(方正教务系统)




前言(开发对象/环境)

声明:笔者并非专业前端开发者,只有十分基础的jsDOM操作,所以有些写的不好的地方,敬请见谅!

本人所在学校:浙江经济职业技术学院

开发面向的教务管理系统:校内信息服务网

官方开发者文档:开发者文档

这里声明我是从 2020/11/21 开始进行小爱课程表的开发,主要功能就是实现导入本校的教务系统课程表。今天(2020/11/22)完成并自测通过了,就等审核通过上线啦。现在就来记录一下这次开发。

荣誉Coder截图

下面我们就来详细盘一盘开发过程中的需要注意的地方,以及一些我的思路。

准备

开发之前我们要明确使用什么样的语言开发?

答案当然使用Javascript,我们需要从网页上获取到课程表的HTML代码,理所当然使用JS对其进行解析

开发者工具:AlSchedule Devtools (浏览器插件)

下载地址:AlSchedule Devtools下载

使用方式:

  1. 将下载的ZIP压缩包解压

  2. 浏览器地址栏中输入 Edge浏览器 --> edge://extensions/ Chrome浏览器 --> chrome://extensions/

  3. 在浏览器中打开开发者选项,导入AISchedule DevTools文件夹

    浏览器扩展管理

OK,到这里我们的开发环境就准备好了。

编程前分析

在Chrome打开一个新的Tab,打开、登陆自己的教务系统,进入课程页面(比如下图展示课程表)。

示例课程表

然后在网页内右键「检查」或者按下F12,打开Chrome开发者工具,会有新增的AISchedule标签,进入后请跟随新手引导,创建「适配项目」。

AISchedule

注意:在创建项目时,教务系统URL一栏要填写的是:你的教务系统的登录界面首页,而不是访问你的课程表的url。这点很重要,因为再后期的E2E自测时,手机会根据你给的URL自动访问教务系统,需要你的手动登录,否则是获取不了你的课程表的。

当你在创建好适配项目后,工具会为你创建一个默认的代码模板,分别是两个函数 provider/parser

provider函数:是是用来获取html的函数,将获取到的html传给 Parser 进行数据处理

输入参数:iframeContent = "",frameContent = "",dom = document(是当前网页的html代码)

返回结果:格式必须是字符串,该结果会被传递给parser函数进行解析

provider函数

parser函数:获取provider提取的html,从中截器对应的课程表信息,再封装为规定的json格式数据返回

输入参数:html,是provider函数的返回值,是包含了所有的课程信息的字符串

返回结果:是json格式的字符串,scheduleHtmlParser函数的输出须符合以下数据结构

输出数据结构
{
    "courseInfos": [
      {
        "name": "数学",
        "position": "教学楼1",
        "teacher": "张三",
        "weeks": [
          1,
          2,
          3,
          4,
          5,
          6,
          7,
          8,
          9,
          10,
          11,
          12,
          13,
          14,
          15,
          16,
          17,
          18,
          19,
          20
        ],
        "day": 3,
        "sections": [
          {
            "section": 2,
            "startTime": "08:00",//可不填
            "endTime": "08:50"//可不填
          }
        ],
      },
      {
        "name": "语文",
        "position": "基础楼",
        "teacher": "荆州",
        "weeks": [
          1,
          2,
          3,
          4,
          5,
          6,
          7,
          8,
          9,
          10,
          11,
          12,
          13,
          14,
          15,
          16,
          17,
          18,
          19,
          20
        ],
        "day": 2,
        "sections": [
          {
            "section": 2,
            "startTime": "08:00",//可不填
            "endTime": "08:50"//可不填
          },
          {
            "section": 3,
            "startTime": "09:00",//可不填
            "endTime": "09:50"//可不填
          }
        ],
      }
    ],
    "sectionTimes": [
      {
        "section": 1,
        "startTime": "07:00",
        "endTime": "07:50"
      },
      {
        "section": 2,
        "startTime": "08:00",
        "endTime": "08:50"
      },
      {
        "section": 3,
        "startTime": "09:00",
        "endTime": "09:50"
      },
      {
        "section": 4,
        "startTime": "10:10",
        "endTime": "11:00"
      },
      {
        "section": 5,
        "startTime": "11:10",
        "endTime": "12:00"
      },
      {
        "section": 6,
        "startTime": "13:00",
        "endTime": "13:50"
      },
      {
        "section": 7,
        "startTime": "14:00",
        "endTime": "14:50"
      },
      {
        "section": 8,
        "startTime": "15:10",
        "endTime": "16:00"
      },
      {
        "section": 9,
        "startTime": "16:10",
        "endTime": "17:00"
      },
      {
        "section": 10,
        "startTime": "17:10",
        "endTime": "18:00"
      },
      {
        "section": 11,
        "startTime": "18:40",
        "endTime": "19:30"
      },
      {
        "section": 12,
        "startTime": "19:40",
        "endTime": "20:30"
      },
      {
        "section": 13,
        "startTime": "20:40",
        "endTime": "21:30"
      }
    ]
  }
parser函数

注意:这里的两个函数除了函数名以外都是可以编辑的,但是由于不了解其中的运行方式,所以我建议不改动函数名及其参数,只编辑其函数体实现功能。

还需要注意的一点就是:scheduleHtmlParser函数:输入课程页面的HTML字符串,提取课程信息,按约定的格式输出JSON。因为这部分是在服务端解析的,用到了cheerio的环境,所以其中不包含document和window对象!

OK,到这里我们就可以开始编程实践了!

开发

provider函数:

这里provider函数需要我们先对网页进行解析,获取到对应所需的课程表内信息,传递给下一个函数

首先我先去看了我的课程表网页的源代码。

发现课程表的网页代码是被嵌入在了当前网页的一个 iframe标签中,并且该标签有一个 ID属性,id=“iframeautoheight”

并且课程表是在当前网页的子网页的一个 table标签 中,并且该标签也有一个 ID属性,id=“Table1”

考虑到在 parser函数 中不能使用document和window对象,就需要使用 cheerio 进行解析,但是笔者只会一点简单的JS,所以我决定不再 parser函数 中进行解析,将解析操作全部放在 provider函数 中完成,返回处理好的结果(自定义结构)。

所以,我先在 provider函数 完成所有解析,取出网页课程表中所有的课程信息,组成一个字符长串,使用 }{ 作为每个课程信息的分隔符,最后返回这个字符串交给下一个函数

function scheduleHtmlProvider(iframeContent = "", frameContent = "", dom = document) {
            //解析后的课程表
            table1 = dom.querySelector("#iframeautoheight").contentWindow.document.querySelector("#Table1")
            //考虑到在 parser函数中解析比较困难,所以我们现在 本函数中解析好后传递给parser
            //约定格式为 }{ 区分不同的td中的文本
            courseInfoString = ""
            all_td_tags = table1.getElementsByTagName("td");
            for( var j=0 ; j<all_td_tags.length ; j++){
                tdContext = all_td_tags[j].innerHTML
                //判断 td标签中的信息是否含有 br标签,不含有就不是课程信息
                if(tdContext.indexOf("<br>") == -1){
                    continue;
                }
                courseInfoString = courseInfoString + tdContext + "}{"
            }
            return courseInfoString
}

执行函数后弹出了的窗口包括了课程表信息,就代表截取成功啦!如下图

provider函数运行成功截图
parser函数:

parser函数,获取到 provider 解析后得到的课程信息字符串(课程信息使用 }{ 进行分隔),所以获取到首先就使用进行分离得到一个包含粗略课程信息的数组(contextArry)。

但是这并不是最终的所有课程信息,因为有些课程会有停课周,以及换课周,所以后续还要进行更细致的分割,最终获得稍详细的课程信息数组(InfoArry)。

根据函数返回数据结构可知,除了要返回课程信息数组(courseInfos),还要返回的是包含每一节课上课信息的数组(sectionTimes),

因此我们先创建一个存储一天中每节课信息的数组(sectionTimes),并将其组合完整。

接下来就要解析获得的为全部解析的课程信息数组了,循环数组,根据每个数组中每个元素进行解析,提取出每一个课程所需的信息,组成一个课程对象,并追加到要返回的课程信息数组中。

注意:这里需要注意几个十分忽略的点

  1. 需要考虑单双周的问题,有些课程是区分单双周的
  2. 需要课程课程存在停课几周的可能,一般会有注明停课时间,
function scheduleHtmlParser(html) {
            // contextArry 存放 td标签 中获得内容,即上课信息
            contextArry = html.split("}{");
            //将表格中解析得到的所有信息都存储在一个数组中
            InfoArry = new Array();
            //定义一个数组,设置每节课对应的开始结束时间
            courseStartTime = ["","08:10","08:55","09:40","10:25","11:10","13:40","14:25","15:10","15:55","16:40","18:00","18:45","19:30"];
            courseEndTime = ["","08:50","09:35","10:20","11:05","11:50","14:20","15:05","15:50","16:35","17:20","18:40","19:25","20:10"];
            //定义星期数组
            weekArry = {"周一" : 1,"周二" : 2,"周三" : 3,"周四" : 4,"周五" : 5,"周六" : 6,"周日" : 7};
            //定义课时信息,并赋值
            sectionTimes = [];
            for(var i = 1 ; i<=13 ; i++){
                sectionTimes.push({"section" : i , "startTime" : courseStartTime[i] , "endTime" : courseEndTime[i]});
            };
            //定义要返回的结果信息
            result = new Array();
            //只需要遍历 上一个函数传过来的 字符串进行分割后的数组
            for(var t=0 ; t<contextArry.length-1 ; t++){
                if(contextArry[t].indexOf("<br><br>") == -1){
                    InfoArry.push(contextArry[t]);
                }else{
                    //注意这里会有 换课的课程 也就是出现 <br><br> 但是后面就没有了的情况,所以需要判断
                    context_split = contextArry[t].split("<br><br>")
                    InfoArry.push(context_split[0]);
                    for(var f=2 ; f<context_split.length ; f++){
                        if(context_split[f].length > 0 & context_split[f].indexOf("</font>") == -1){
                            InfoArry.push(context_split[f]);
                        }
                    }
                }
            }
            //解析获取到的所有课程信息
            for(var i = 0 ; i < InfoArry.length ; i++){
                courseInfo = InfoArry[i].split("<br>");
                // ["分布式存储计算系统", "周二第1,2节{第2-17周}", "王义勇", "信息科学楼8-4"]
                name = courseInfo[0];
                position = courseInfo[3];
                teacher = courseInfo[2];
                day = weekArry[courseInfo[1].substring(0,2)];
                //对上课时间进行截取 得到的只有 上课时间和持续的周数
                courseInfo[1] = courseInfo[1].substring(2,courseInfo[1].length-1); //第1,2节{第2-17周
                
                str1 = courseInfo[1].split("{")[0]; //第1,2节
                str2 = courseInfo[1].split("{")[1]; //第2-17周

                str1 = str1.substring(1,str1.length-1); //1,2
                str2 = str2.substring(1,str2.length-1); //2-17

                arry1 = str1.split(","); // {"1", "2"}
                arry2 = str2.split("-"); // {"2", "17"} 当该数组最后存在 -1/-2 时,表示该课程分单/双周

                weeks = [];
                sections = [];

                // arry1(上课的节数,如:1,2) 转化数组内的值类型 (字符串 > 数字),并根据数组中的内容 找对应的 sections
                for(var a=0 ; a<arry1.length ; a++){
                    arry1[a] = parseInt(arry1[a])
                    sections.push(sectionTimes[arry1[a]-1])
                }
                arry2[0] = parseInt(arry2[0])

                // arry2(上课的周数 如:2-5) 转化数组内的值类型 (区分单双周)
                for(var b=0 ; b<arry2.length ; b++){
                    if(arry2[b].length > 2 & b == 1){
                        var temp_str = arry2[b]
                        arry2[b] = parseInt(temp_str.substring(0,2));
                        if(temp_str.substring(2).split("|")[1] == "双"){
                            arry2[b+1] = -2;
                        }else{
                            arry2[b+1] = -1;
                        }
                    }else{
                        arry2[b] = parseInt(arry2[b])
                    }
                }
                //循环 arry2 ,获取要上课的周
                for(var c=arry2[0] ; c <= arry2[1] ; c++){
                    //区分单双周
                    if(arry2.length > 2 ){
                        //单周
                        if(arry2[2] == -1 & c%2 == 1 ){
                            weeks.push(c);
                        }
                        //双周
                        if(arry2[2] == -2 & c%2 == 0){
                            weeks.push(c);
                        }
                    }else{
                        weeks.push(c);
                    }
                }
                course = {"name" : name , "position" : position , "teacher" : teacher , "weeks" : weeks , "day" : day, "sections" : sections};
                result.push(course);
            };
            return { courseInfos: result , sectionTimes : sectionTimes}
}

执行函数后弹出了的窗口包括了正确的格式的返回值信息,并且控制台输出 All run Successfully,就代表截取成功啦!如下图

parser函数运行成功截图 console运行成功截图

OK,到这里说明以上两个函数都运行没有问题了,这样可以提交了(需要先登录哦)!

E2E自测

在提交完代码后,在插件左侧就会出现一个刚提交的代码版本号,并显示为 E2E自测

接下就可以到手机端进行自测了,打开vControl选项

在手机端打开课程表的教务导入功能,搜索学校,选择自己提交的适配,你可亲自体验,验证可用性。如果你觉得没问题,请点击反馈按钮「完美」,至此,你的适配已完成,状态为审核中。

自测1 自测2 自测3

如果未导入成功,请查看右下角vControl中的log->info,核对返回值:

vControl检查

到这里如果自测通过那么开发就算成功了,反馈是 完美 ,程序会自动提交给进行内部审核,审核通过后就可以发布使用啦!

接下来就只要坐等审核通过上线就好了!(生怕有bug又打回来)

提交审核时间:2020-11-22 16:39:12 星期日

。。。。。。

测试通过了,啊哈哈哈!项目已经上线,可以正式面向全校师生使用了。

online

现在使用小爱课程表,选择学校:【 浙江经济职业技术学院-教务管理 】,找到自己的课表,点击【 一键导入 】,就可以使用了 !

online

好了,今天的分享到这里就结束了,希望此刻的你会对 小爱课程表 有更深入的了解。
如果有什么问题的话可以在下方留言哦(留言我可能不容易看到,也可以私信给我)。
欢迎关注我的个人博客!2020-11-23 15:04:28 星期一


  • 发表时间:2020-11-23
  • 版权声明:自由转载-非商用-非衍生-保留署名
  • 评论

    姜家伟-jiawei15214742755@163.com
    博主
    由于后台没设置评论提醒,所以可能看到会比较慢,如果有什么问题也可以私信给我!
    留言