MENU

西北工业大学翱翔门户课表信息解析小记

2020 年 01 月 12 日 • 技术

写在前面

折腾这个的缘由是为了给WakeUp课程表适配🍉大的课表导入。由于学校的门户是用ajax来读取并写网页body的,直接用WakeUp课程表的Webview方式导入获取不到课表信息,所以只能模拟登录后带参数请求原始课表数据然后手动解析。具体PR地址:#21 #22

最后,引用一句话:

曾经有一位程序员遇到了一个问题,他打算用正则表达式来解决。
现在他就有两个问题了。

——某位睿智的阿拉伯人

模拟登录

模拟登录没有什么好说的,先get一下登录页面写一下cookies,然后再发post包,具体post内容为

Jsoup.connect(**"http://us.nwpu.edu.cn/eams/login.action"**).headers(headers).cookies(cookies)  .data(**"username"**, id).data(**"password"**, pwd).data(**"encodedPassword"**, "").data(**"session\_locale"**, "zh\_CN")
        .timeout(5000).method(Connection.Method.**POST**).execute()

其中的cookies为第一步get后需要设置的值,headers就是Host值和linux下Firefox的UA。

之后的返回body根据具体情况,会有成功登录、账户不存在、密码错误等情况,不过假如多次尝试失败之后再尝试登录,可能会要求输入验证码。考虑到各种原因,就没有具体对验证码的处理了,直接提示换网络环境。

获取 ids

这个当初卡了比较久。ids的值会以js动态写入的方式,在网页body里面生成一个hidden的input,之后post获取课表信息的时候,浏览器会自动把它携带进请求里面,服务器验证ids正确之后才会返回课表信息。ids的值与学号有关,而且是成线性关系的,不过好像不是在前端加密的,而是直接请求后端的时候就会把具体的ids值返回过来。需要做的便是请求一个能得到具体ids值的地址,然后再解析一下返回文本提取ids。

Jsoup.connect(**"http://us.nwpu.edu.cn/eams/courseTableForStd.action"**).headers(headers).cookies(cookies)  .timeout(5000).method(Connection.Method.**GET**).execute()._let_ {
            ids = Regex("form,\\"ids\\",\\"\\\\d+?(?=\\")").find(it.body())!!.value._replace_("form,\\"ids\\",\\"", "")
        }

获取具体学年学期课表的id

这个叫semester.id,是为了指定具体的学年学期课表,要先获取教务系统提供的所有学年学期对应的id,然后再根据用户选择的学年学期来获取对应的id。

Jsoup.connect(**"http://us.nwpu.edu.cn/eams/dataQuery.action"**).headers(headers).cookies(cookies)  .data(**"tagId"**, "semesterBar15920393881Semester").data(**"dataType"**, "semesterCalendar").data(**"empty"**, "true")
        .timeout(5000).method(Connection.Method.**POST**).execute()._let_ {
            var semestersname: String = "秋春夏"
            val foundResults = Regex("(?<=id:)\\\\d+(?=,)").findAll(it.body())
            for (findText in foundResults) {
                semestersid = findText.value
                if (it.body()._contains_(regex = Regex(pattern =
                        "$semestersid,schoolYear:\\"$semestersyear-\\\\d+\\",name:\\""
                                + semestersname\[semestersterm._toInt_() - 1\].toString() + "\\""))) {
                    break
                } else {
                    semestersid = "NOT\_MATCH"
                }
            }

请注意,教务系统是用秋、春、夏来指定学期的,app分别传参1、2、3

获取具体课表的原始数据

为什么叫原始数据呢,因为这个是从网络请求中捕获的,是以类似执行脚本的方式来传送课表数据,再绘制在网页上的。ids和semester.id都是必填的,前面步骤已经获取了对应的值。

Jsoup.connect(**"http://us.nwpu.edu.cn/eams/courseTableForStd!courseTable.action"**).headers(headers).cookies(cookies)
        .data(**"ignoreHead"**, "1").data(**"setting.kind"**, "std").data(**"startWeek"**, "1").data(**"project.id"**, "1")
        .data(**"semester.id"**, semestersid).data(**"ids"**, ids)
        .timeout(5000).method(Connection.Method.**POST**).execute()

返回的body内容很多,我们主要聚焦以下部分(以下仅为例子)

<script language="JavaScript">
// function CourseTable in TaskActivity.js
var table0 = new CourseTable(2018,91);
var unitCount = 13;
var index=0;
var activity=null;
activity = new TaskActivity("6966","陈俊梅","36856(U31G71001G.07)","体育1(武术)(U31G71001G.07)[长安校区]","3567","[体育场地]C0902云天苑田径场跑廊","00001111111111111111000000000000000000000000000000000");
index =1*unitCount+10;
table0.activities[index][table0.activities[index].length]=activity;
index =1*unitCount+11;
table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("8730","李伟刚","38938(U14M11059.01)","离散数学(U14M11059.01)[长安校区]","1458","[教学西楼C座]教西C206","01111111111111111000000000000000000000000000000000000");
index =3*unitCount+2;
table0.activities[index][table0.activities[index].length]=activity;
index =3*unitCount+3;
table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("8730","李伟刚","38938(U14M11059.01)","离散数学(U14M11059.01)[长安校区]","1458","[教学西楼C座]教西C206","01111111011111111000000000000000000000000000000000000");
index =1*unitCount+2;
table0.activities[index][table0.activities[index].length]=activity;
index =1*unitCount+3;
table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("8730","李伟刚","38938(U14M11059.01)","离散数学(U14M11059.01)[长安校区]","-1","[&nbsp;]停课","00000000100000000000000000000000000000000000000000000");
index =1*unitCount+2;
table0.activities[index][table0.activities[index].length]=activity;
index =1*unitCount+3;
table0.activities[index][table0.activities[index].length]=activity;
table0.marshalTable(2,1,20);
fillTable(table0,7,13,0);
</script>

分析转换原始数据

观察后发现,以activity开头的代码会提供一门“新“课程的老师、名称、地点和上课周(下标表示周数,第一个字符是凑数用的)等信息。为什么”新“有冒号呢,因为你会发现其有可能确实是截然不同的两门课程,也有可能只是或上课周或上课地点(顺带可能变了老师)变了。倘若是截然不同的课,在WakeUp课程表中要存成两个baseList信息,但假如只是一门课的周一和周三两个时间段的信息,则baseList只要存一个。请注意,activity的出现顺序是有规律的,相同名称的课程一定会集中出现,其次才是真正的不同课程的出现。怎么处理呢,按下不提先继续分析。

在activity之后的以index开头的几行,每一行提供了针对上文activity课程的一个星期时间的一个课时间。什么意思呢,比如

index =1unitCount+2;  
index =1unitCount+3;

分别指定这门课会在星期(1+1=2)的第(2+1=3)节和第(3+1=4)节上课。针对实际样本发现,每个activity下的index只会指定同一星期时间的具体课时间。倘若该课在周二和周四均上课,那么会再有一个activity及其后续index来指定周四的具体时间安排。

table0和var开头的语句不会提供有效消息,所以可以直接忽略。读取到table0.marshalTable的时候便可以停止分析了。

所以梳理一下,首先有activity来提供某课程基本信息,其后有index来提供仅一个星期时间的具体课时间。倘若该课一周上多次,则其后再跟着activity和另一组index来提供其他周时间的具体课时间。若该课程由多老师、多地点上课,或者由于停课补课等原因出现新课时等情况,则也是按相关度顺序,以其他的activity即index提供信息。简要而言,一门课,会有多组activity-index来提供信息。而WakeUp课程表的存储逻辑是同名课程存一个baseList提供基本消息,具体不同的老师时间上课地点多次存detailList,两者间保持id相同以正确联系。而且detailList中是以startWeek和endWeek来将上课周连续存储,而我校的上课周原始数据是以01方式来存储,可能并不连续(比如中间有一周老师有事停课之类的)。所以针对01week还要专门读取连续周数据并多次存detailList(即一个activity-index可能会存多次detailList)。

所以具体实现逻辑如下,先创建一些last变量表示上一个课程的信息,每次读入activity的时候将上一个activity信息先存入baseList,然后再处理当下activity并写入last变量,其中l01week表原始数据中的01表示的上课周信息,lstartendweek以开始周-结束周-开始周-结束周…这样的顺序来存储数据,之后每两个作为一个配对写一次baseList:

**var** lteacher: String = **""**_//__以下为__"last"__的意思  
_**var** lclass: String = **""  
****var** lroom: String = **""  
****var** l01week: String = **""  
****var** lstartendweek: MutableList<Int> = _mutableListOf_<Int>()_//__一先一后分别为开始和结束_

以下为temp之意的变量专门用来处理l01week转lstartendweek中的if逻辑,以及逐行读取时候的信息传递:

**var** tstartweek: Int = -1  
**var** ttday: Int = -1  
**var** ttstartNode: Int = -1  
**var** ttstep: Int = 0  
**var** firstornot: Boolean = **true  
****var** skipornot: Boolean = **false**_//__当__activity__中有_ _-1_ _值的时候,可能说明这个课情况特殊(比如说是停课状态),就直接__skip_

其中firstornot用来表示是不是第一次遇到index,因为理论上连续index表示连续的课时间,第一次遇到index的时候存一下开星期时间和开始节时间,之后的index就step+1表示又多上了一节时间。

其后正式逐行读取时,以var和table0开头的行直接continue掉。

若以activity开头:若lstartendweek不为空,说明之前还有activity对应的index没有存,先将上一个activity对应的index基本信息存入detailList。然后开for循环逐个读l01week字符并将连续1片段(单个1也算连续1片段)的开头结尾写入lstartendweek。之后,倘若本次的activity课程名和上次的(lclass)不同,则要将本次读取的课程信息写baseList;倘若相同(则说明老师周地点等会出现不同),则baseList不需动,但之后(读取到下一个activity)仍然要写detailList(相当于和上次写的baseList建立联系)。

若以index开头,先判断当下是不是该activity第一个index,若是则写ttday等信息;若不是(说明是第二第三),则仅仅ttstep自加一说明又多上了一节课。请注意,这些index信息是在遇到下一个activity时候写进detailList的。

基本如此,最后调用write2DB(),利用baseList和detailList信息来导入课程数据。

最后编辑于: 2020 年 07 月 24 日
返回文章列表 文章二维码
本页链接的二维码
打赏二维码