小爱课程表 适配 广东工业大学

小爱课程表 适配 广东工业大学

在适配广东工业大学的小爱课程表过程中,我遇到了一些挑战,包括处理递归收集HTML导致的性能问题,以及校区信息的自动设置。文章详细介绍了新旧版本的优化,自动设置上课时间的逻辑,以及如何通过AIScheduleSelects实现用户选择校区的功能。

小爱课程表官方文档及工具:https://ldtu0m3md0.feishu.cn/docs/doccnhZPl8KnswEthRXUz8ivnhb

新版本文档博客:https://lacus.site/2021/12/ai-schedule/

课表新版本的优化

递归与HTML

原版本使用递归收集每个iframe的HTML,使得最终收集到的HTML非常大,严重拖慢了正则匹配的速度

新版本利用iframe的src,只保存个人课表页面和所有的iframe-src,加快了用户手机端正则匹配速度

自动设置每节课上课时间

原版本考虑自动设置每节课上课时间,就要通过请求获取校区信息,而教务系统的校区信息和身份证等敏感信息放在一起,为了避嫌只能放弃

新版本提供AIScheduleSelects实现用户选择校区,在scheduleTimer中实现

代码

async function scheduleHtmlProvider(recursion = false, ifrsHTML = {list:[]}, ifrSrc = "", dom = document) {
    //除函数名外都可编辑
 
    // 如果找到个人课表的iframe就保存起来
    if(ifrSrc.indexOf("xsgrkbcx!xsAllKbList.action") != -1){
        ifrsHTML.kb = dom.querySelector('body').outerHTML;
    }
    // 把iframe的src保存起来
    ifrsHTML.list.push(ifrSrc);
    // 递归iframe树,保存src和个人课表
    const ifrs = dom.getElementsByTagName("iframe");
    for(let i = 0; i < ifrs.length; i++){
        const ifrDom = ifrs[i].contentWindow.document;
        scheduleHtmlProvider(true, ifrsHTML, ifrs[i].src, ifrDom);
    }
    // 递归时不执行匹配,直接返回
    if(recursion){
        return null;
    }
 
    const regex_kbxx = /kbxx\s=\s(\[{.*}\])/m;
    const regex_hqxq = /xnxqdm=(\d{6})/m;
    const regex_dlsj = /登录时间:(\d{4})-(\d{2})/m;
    
    // 有个人课表且是全部周就直接返回数据
    if(ifrsHTML.kb != null){
        let kbxx = regex_kbxx.exec(ifrsHTML.kb);
        if(kbxx != null){
            return kbxx[1];
        }
    }
    
    let xqdm = "";
    // 通过src获取最后一个iframe的src中的学期代码
    for(let i = ifrsHTML.list.length-1; i >= 0; i--){
        let regResult = regex_hqxq.exec(ifrsHTML.list[i]);
        if(regResult != null){
            xqdm = regResult[1];
        }
    }
    // 假如src中都没有就根据登录时间获取
    if(xqdm === ""){
        let dlsj = regex_dlsj.exec(dom.querySelector('body').outerHTML);
        let mm = parseInt(dlsj[2]);
        if(mm>=2&&mm<=7){
            xqdm += (parseInt(dlsj[1])-1);
            xqdm += "02";
        }else{
            xqdm += mm[1];
            xqdm += "01";
        }
    }
    // 根据学期代码发送请求获取数据
    let request = new XMLHttpRequest();
    request.open('GET', "/xsgrkbcx!xsAllKbList.action?xnxqdm="+xqdm, false); 
    request.send(null);
    if (request.status === 200) {
        return regex_kbxx.exec(request.responseText)[1];
    }
    return null;
 
}
function scheduleHtmlParser(html) {
    //除函数名外都可编辑
    //传入的参数为上一步函数获取到的html
    //可使用正则匹配
    //可使用解析dom匹配,工具内置了$,跟jquery使用方法一样,直接用就可以了,参考:https://juejin.im/post/5ea131f76fb9a03c8122d6b9
    //以下为示例,您可以完全重写或在此基础上更改
    
    //html中的json拿过来
    let kbxxjson = JSON.parse(html);
 
    //组织返回json
    let courseInfos = new Array();
    kbxxjson.forEach(function (item) {
        let course = {};
        course.name = item.kcmc;
        course.position = item.jxcdmcs;
        course.teacher = item.teaxms;
        let weekarr = item.zcs.split(',');
        course.weeks = [];
        weekarr.forEach(function (week) {
            course.weeks.push(parseInt(week));
        });
        course.weeks.sort((a,b)=>{return a-b});
        course.day = item.xq;
        course.sections = [];
        let sectionsarr = item.jcdm2.split(',');
        sectionsarr.forEach(function (section) {
            course.sections.push({"section" : parseInt(section)});
        });
        courseInfos.push(course);
    });
    console.log(courseInfos);
    return courseInfos;
}
/**
 * 时间配置函数,此为入口函数,不要改动函数名
 */
async function scheduleTimer() {
  const longdong_dongfenglu = {startWithSunday:false,showWeekend:true,forenoon:4,afternoon:5,night:3,sections:[{section:1,startTime:'08:15',endTime:'09:00',},{section:2,startTime:'09:05',endTime:'09:50',},{section:3,startTime:'10:10',endTime:'10:55',},{section:4,startTime:'11:00',endTime:'11:45',},{section:5,startTime:'13:30',endTime:'14:15',},{section:6,startTime:'14:20',endTime:'15:05',},{section:7,startTime:'15:10',endTime:'15:55',},{section:8,startTime:'16:15',endTime:'17:00',},{section:9,startTime:'17:05',endTime:'17:50',},{section:10,startTime:'18:30',endTime:'19:15',},{section:11,startTime:'19:20',endTime:'20:05',},{section:12,startTime:'20:10',endTime:'20:55',},]}
  const daxuecheng_jieyang = {startWithSunday:false,showWeekend:true,forenoon:4,afternoon:5,night:3,sections:[{section:1,startTime:'8:30',endTime:'9:15',},{section:2,startTime:'9:20',endTime:'10:05',},{section:3,startTime:'10:25',endTime:'11:10',},{section:4,startTime:'11:15',endTime:'12:00',},{section:5,startTime:'13:50',endTime:'14:35',},{section:6,startTime:'14:40',endTime:'15:25',},{section:7,startTime:'15:30',endTime:'16:15',},{section:8,startTime:'16:30',endTime:'17:15',},{section:9,startTime:'17:20',endTime:'18:05',},{section:10,startTime:'18:30',endTime:'19:15',},{section:11,startTime:'19:20',endTime:'20:05',},{section:12,startTime:'20:10',endTime:'20:55',},]}
  const panyv = {startWithSunday:false,showWeekend:true,forenoon:4,afternoon:5,night:3,sections:[{section:1,startTime:'8:30',endTime:'9:15',},{section:2,startTime:'9:20',endTime:'10:05',},{section:3,startTime:'10:20',endTime:'11:05',},{section:4,startTime:'11:10',endTime:'11:55',},{section:5,startTime:'13:40',endTime:'14:25',},{section:6,startTime:'14:30',endTime:'15:15',},{section:7,startTime:'15:20',endTime:'16:05',},{section:8,startTime:'16:15',endTime:'17:00',},{section:9,startTime:'17:05',endTime:'17:50',},{section:10,startTime:'19:00',endTime:'19:45',},{section:11,startTime:'19:50',endTime:'20:35',},{section:12,startTime:'20:40',endTime:'21:25',},]}
 
  await loadTool('AIScheduleTools')
  const userSelect = await AIScheduleSelect({
    titleText: '选择校区',
    contentText: '以便设置每一节课的开始和结束时间', 
    selectList: [
      '大学城',
      '龙洞',
      '东风路',
      '揭阳',
      '番禺'
    ],
  })
  if(userSelect === '大学城'){
    return daxuecheng_jieyang;
  }else if(userSelect === '龙洞'){
    return longdong_dongfenglu;
  }else if(userSelect === '东风路'){
    return longdong_dongfenglu;
  }else if(userSelect === '揭阳'){
    return daxuecheng_jieyang;
  }else if(userSelect === '番禺'){
    return panyv;
  }
  return {};
}

更好用的版本更新

在给其它同学使用过以后,发现许多同学都不会选择全部周次,而小爱课程表没有相关提示。群里有小伙伴写了可以自动发送请求的代码,于是我也打算看看能不能改一下,使之在任何界面都可以获取到课程信息。

逻辑

首先判断是不是正确打开课表并选择全部周次?如果正确打开课表并选择全部周则获取到的内容中就包含这段json;如果没正确打开就发送请求,发送请求中需要传学年学期的参数。我先判断html里是是否有已选择的学年学期,有的话直接获取,没有的话按照登录时间生成一个。

实现过程

  • 首先看查询课表时候的网络请求,最后找了一圈发现是藏在课表信息js代码里的。
  • 查看Provider得出的结果,发现课表信息存在,为json字符串。
  • img
  • 利用Provider里frameContent做递归次数统计,当在最后一次返回的时候进行课表状态的判断,决定是直接拿还是发送请求,最后运用正则匹配出课表信息的json。
  • 返回这个json字符串后用JSON.parse(html)转一下就行了。

实现代码

function scheduleHtmlProvider(iframeContent = "", frameContent = "", dom = document) {
    //除函数名外都可编辑
    //以下为示例,您可以完全重写或在此基础上更改
                                
    const ifrs = dom.getElementsByTagName("iframe");
 
    if (ifrs.length) {
        for (let i = 0; i < ifrs.length; i++) { 
            const dom = ifrs[i].contentWindow.document; 
            frameContent += "a"; 
            iframeContent += scheduleHtmlProvider(iframeContent, frameContent, dom); 
            frameContent = frameContent.substring(0,frameContent.length-1); 
        } 
    } 
    if(!frameContent.length){ 
        let regex_kbxx = /kbxx\s=\s(\[{.*}\])/m; 
        let regex_hqxq = /xnxqdm=(\d{6})&amp/m; 
        let regex_dlsj = /登录时间:(\d{4})-(\d{2})/m; 
        let html = dom.getElementsByTagName('html')[0].innerHTML + iframeContent; 
        let kbxx = regex_kbxx.exec(html); 
        if(kbxx == null){//判断是否选了全部周 
            let url = "/xsgrkbcx!xsAllKbList.action?xnxqdm="; 
            let xq = regex_hqxq.exec(html);
            if(xq != null){//判断是否打开了个人课表页 
                //直接获取选择的学年学期 
                url += xq[1]; 
                console.log("没选全部周或打开了班级课表"); 
            }else{ 
                let time = regex_dlsj.exec(html);//什么都没打开就通过登录时间判断学年学期 
                let yyy = parseInt(time[2]); 
                if(yyy>=2&&yyy<=7){
                    url += (parseInt(time[1])-1);
                    url += "02";
                }else{
                    url += time[1];
                    url += "01";
                }
                console.log("没打开课表");
            }
 
            let request = new XMLHttpRequest();
            request.open('GET', url, false); 
            request.send(null);
            if (request.status === 200) {
                kbxx = regex_kbxx.exec(request.responseText);//没选全部周就请求全部周次的课表
            }
        }
        return kbxx[1];
    }
    
    if(!ifrs.length){
        return dom.querySelector('body').outerHTML
    }
 
    return dom.getElementsByTagName('html')[0].innerHTML + iframeContent  
}
function scheduleHtmlParser(html) {
    //除函数名外都可编辑
    //传入的参数为上一步函数获取到的html
    //可使用正则匹配
    //可使用解析dom匹配,工具内置了$,跟jquery使用方法一样,直接用就可以了,参考:https://juejin.im/post/5ea131f76fb9a03c8122d6b9
    //以下为示例,您可以完全重写或在此基础上更改
    
    //html中的json拿过来
    let kbxxjson = JSON.parse(html);
 
    //组织返回json
    let courseInfos = new Array();
    kbxxjson.forEach(function (item) {
        let course = {};
        course.name = item.kcmc;
        course.position = item.jxcdmcs;
        course.teacher = item.teaxms;
        let weekarr = item.zcs.split(',');
        course.weeks = [];
        weekarr.forEach(function (week) {
            course.weeks.push(parseInt(week));
        });
        course.weeks.sort((a,b)=>{return a-b});
        course.day = item.xq;
        course.sections = [];
        let sectionsarr = item.jcdm2.split(',');
        sectionsarr.forEach(function (section) {
            course.sections.push({"section" : parseInt(section)});
        });
        courseInfos.push(course);
    });
    console.log(courseInfos);
    return {"courseInfos" : courseInfos};
}

第一个辣鸡版本

在scheduleHtmlParser方法中有如下注释

传入的参数为上一步函数获取到的html 可使用正则匹配 可使用解析dom匹配,工具内置了$,跟jquery使用方法一样,直接用就可以了,参考:https://juejin.im/post/5ea131f76fb9a03c8122d6b9

因想着快速开发出来,所以直接查看网页源代码,发现有如下规律:

img

每个id都是由 第几节-星期几 构成的,可以使用jquery类似方法获取信息。

第一个版本的关键代码就出来了:

     for(let lesson =1;lesson<=14;lesson++){
        for(let week=1;week<=7;week++){
            let serchid = '#'
            if(lesson<=9){
                serchid+='0'
            }
            serchid+=(lesson+'-'+week)
            //寻找课程by(第几节-星期几)
            let getCourse = $(serchid)
            let everyTime = getCourse[0].children[0].children
            
            for(let i=0;i<everyTime.length;i++){//记录课程编号
                let courseStr = everyTime[i].attribs.title//这里就是课程信息了 后面存进数组就行了
            }
        }
    }

因为学校安排的课程都是两节课以上连在一起的,所以需要在前面先循环一次拿到课程编号,判断出重复的课程,最后再组织返回。

img

img

LICENSED UNDER CC BY-NC-SA 4.0
Comment