小爱课程表官方文档及工具: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字符串。
- 利用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})&/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
因想着快速开发出来,所以直接查看网页源代码,发现有如下规律:
每个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//这里就是课程信息了 后面存进数组就行了
}
}
}
因为学校安排的课程都是两节课以上连在一起的,所以需要在前面先循环一次拿到课程编号,判断出重复的课程,最后再组织返回。