thu-learn-lib 开发小记(转)
原创:https://harrychen.xyz/2019/02/09/thu-learn-lib/
今天是大年初五,原本计划出门玩,但是天气比较糟糕就放弃了。想到第一篇博客里面预告了要给thu-learn-lib 写一个小介绍,已经过去了好几天。正好我也不太想写代码,就回来把这个坑填上。它发布在 GitHub 和 npm 上。
前言
顾名思义,这个库是帮助像我这样的菜鸡在清华大学学习程序访问清华大学网络学堂。最早网络学堂是在2001年上线的,这个版本使用最广,页面简介,速度也不错。然而对于程序员,它很不友好:完全没有前端,网页是服务器纯动态生成的,想拿到结构化的信息的唯一办法就是解析DOM元素。由于2001版网络学堂交互逻辑不是很方便,访问各种课程内容(四大核心板块是公告、作业、文件、讨论)都需要较多的点击,出现了一系列助手软件来统一这些信息,让同学们不至于迷失在作业的海洋中。比较好用的一个 Chrome 插件是 Learn Helper,设计精美,功能丰富。
2915年,学校上线了一个新版的网络学堂(https://learn.cic.tsinghua.edu.cn ,目前已经下线了)。2015版学堂的最大特点是:慢。页面的动态内容几乎都是 AJAX 的,然而由于无法解释的原因速度十分的缓慢,并且非常容易出错。这些接口的数据格式相当糟糕,结构化程度甚至还不如直接解析DOM。用2015版的课程不多,很多老师在(不可逆的)升级以后都表示了由衷的后悔之意。Learn Helper也添加了对此版本的支持,但是经常导致加载出错,因此我干脆在其中屏蔽了所有这个版本的课程。
学校可能意识也到2015版过于丢人,所以在2018年下半年推出了目前最新的2018版网络学堂,并在学期结束后一举下线了两个旧版本,把数据都迁移了过去。也就是说,从2018-2019年春季学期开始,就只剩下这一个线上版本了。遇到这么激进的迁移,同学们的各种作业下载/学堂助手小工具都不灵光了,急需一波更新。于是我挺身而出闲来无事,就用 JS 写了一个库,来做信息爬取。
踩坑:网络访问
说实话,这是我自己写的第一个 JS 项目,听了大家的意见,用 TypeScript 写并编译成 ECMAScript 2016,在现代的浏览器 / JS 引擎上应该都没有语言兼容性问题。我天真地以为如今 JS 已经是天下一统了,没想到上来就踩了个大坑。
都说现在 fetch API 非常好用,于是我写了一通以后,丢进 nodeJS 一跑,上来就是一个 'fetch' not defined。这下才知道原来只有浏览器才有 window.fetch,于是又找了一个叫做 cross-fetch 的 ponyfill 库:现在 node 能跑了,然而不能记录 Cookie 信息,于是又需要一个包装,用一个 CookieJar 来做拦截存下这些东西。相反的,在浏览器那边,开发者不需要也不能控制这些东西(甚至读不到 Set-Cookie 的 header,只管请求就是了,浏览器爸爸啥都做好了)。这时候我就非常难受了,在放弃的边缘试探。好在最后找到了一个比较小众的库叫 real-isomorphic-fetch,解决了上述所有不一致的地方。
这个库的设计是还是比较不错的,每一个 fetch 实例对应不同的 CookieJar,可以很方便的做多 session 管理。当然我没有直接支持这个特性,而是在类中暴露出来了对应的 cookieJar 对象,用户也可以在初始化的时候传进自己的。拿回来以后,可以用来做文件的下载,或者自行实现多用户的功能。
不过美中不足的是,这个库可能用的人太少了,没有 typing 信息,导致我不得不用 require 的方法去用它,并且需要关掉 tslint 的 no-var-requires 这条规则。自然,用的时候也会失去一切关于类型的补全信息。
踩坑:网络学堂
开启疯狂吐槽网络学堂2018模式。
2018 版网络学堂是学校信息化技术中心研发部门自行开发的。我们要肯定,它的UI比较美观,甚至还做了响应式设计和移动版;在使用体验上也很好,不管是全局还是课程都有一定的信息汇总,不用像以前那样一个一个页面点开。当然,作为程序员的我心里清楚,用户感觉良好的背后,可能是……*一样的实现。
首先令人惊喜的是,网络学堂的大部分API都是基于GET请求+返回JSON的设计,这对于程序员来说可以说是大礼包了。但是先不要高兴的太早,因为API返回的数据格式大部分类似像这样(一些数据隐去):
{"kssj":1545926400000,"kssjStr":"2018-12-28 00:00","jzsj":1546617599000,"jzsjStr":"2019-01-04 23:59","bt":"第四单元书面作业","wz":13,"zywcfs":1,"zytjfs":2,"jffs":1,"mxdxmc":"全体学生-全体","wlkcid":"2018-2019-126ef84e7689a14e101689a618d890549","xszyid":"26ef84e7689a14e101689a61975b08b4","xh":"[REDACTED]","zyid":"sjqy_26ef84e7689a14e101689a618d890549881519","zynr":null,"zynrStr":"","zyfjid":"[REDACTED]_XSZY_1548778313_1034e0eb-bca0-4555-914f-ed6d0a943572_sjqy01-admin","scr":"","scsj":1546087464000,"scsjStr":"2018-12-29 20:44","gzzh":"2018210974","pysj":1547618929000,"pysjStr":"2019-01-16 14:08","pynr":"","pylj":"","cj":10,"zt":"已交","pyzt":"已批改","qzid":"15061969","bz":"01-admin","qzmc":"全体学生-全体组","xm":"[REDACTED]","dwmc":"[REDACTED]","bm":"[REDACTED]","djzcj":"","jsm":"[REDACTED]","id":null}
第一次看到这些东西的时候,我的脑中充满了黑人问号的表情。是的,拼音首字母命名成的字段。但是人民的智慧是无穷的,我发现拼音输入法+群众的力量对付这些奇怪的名称还是比较有效的。据不科学统计,本次开发约 20% 的时间用于研究这些字段究竟是什么。
如果仅仅如此还好,更可怕的是,这些API显然不是同一批人开发的,格式不同,比如很明显带有 sj 后缀的字段是一个时间,但有些地方它的内容是字符串,有些地方它的内容是 epoch 以来的毫秒数,还有些地方,它的内容始终是 null,而相应的 sjStr 内容代替了它。还有比如代表内容的 nrStr 字段中的 HTML entity 需要解码,而另有一个 nr 字段存放了未经 entity 编码的,而是经过 base64 编码的相应内容。再次据不科学统计,本次开发还有约 30% 的时间用于对付这些奇奇怪怪的返回内容。
老师,能再给力一点吗?
其实原本我做的是对于每一个板块(公告、作业、文件、讨论)独立地爬取内容,但是每个板块的API居然都不!一!!样!!!有一些是 RESTful 的,大部分是 GET 加上请求参数,还有一些甚至是 POST(并且参数极其冗长,还包括了一些数据库的表名等信息,虽然实验证明传过去的这些东西并没有用)。好在最终我发现其实课程的总览页面用到的API全都是基于 GET 的,并且内容非常全,就统一换上了这些。
然而我还忽略了一些问题:API返回的内容不全!还是有一些重要的信息,比如学生提交的作业内容、公告的附件等,被直接嵌入在页面里返回。这意味着我必须要做 DOM 的解析,用到了 cheerio 这个轻量级的,接口类似于 jQuery 的 DOM 解析库。顺便一提,使用它的时候最好关掉 decodeEntities 这个选项,否则所有的中文字都会变成对应的 HTML entity(即 Unicode 码点)。DOM 解析一大麻烦的问题是可能对方的一个小改动就会给我带来大影响,anyway,有问题再更新吧。
如你所见,至少有 40% 时间用来解决这些麻烦的细节问题,还有剩下的 10% 用在和 npm 抗争上。原本这个项目叫做 thu-learn2018-lib,但是 npm 始终认为我是 SPAM (可能因为默认创建的项目里面就有年份,我就被误伤了)。不想发邮件抗议,于是就改成了现在这个名字发布了。
事实上,网络学堂2018还给我们带来了一些惊喜的小feature,比如SQL注入、任意文件下载、用户信息泄露,等等。这篇博客本意不是讲安全,就按下不表了。
效果
我认为这个库的用法还是很简单的,虽然写了 README,这里还是贴一下。
import { Learn2018Helper } from 'thu-learn-lib'; // in JS engines, each instance owns different cookie jars const helper = new Learn2018Helper(); // all following methods are async // first login const loginSuccess = await helper.login('user', 'pass'); // take out cookies (e.g. for file download), which will not work in browsers // its type is require('tough-cookie-no-native').CookieJar console.log(helper.cookieJar); // get ids of all semesters that current account has access to const semesters = await helper.getSemesterIdList(); // get get semester info const semester = await helper.getCurrentSemester(); // get courses of this semester const courses = await helper.getCourseList(semester.id); const course = courses[0]; // get detail information about the course const discussions = await helper.getDiscussionList(course.id); const notifications = await helper.getNotificationList(course.id); const files = await helper.getFileList(course.id); const homework = await helper.getHomeworkList(course.id); const questions = await helper.getAnsweredQuestionList(course.id); // logout if you want, which has no effect in browsers helper.logout();
拿到的数据格式是类似这样的(用一个作业为例):
{"id":"sjqy_26ef84e7689a14e101689a5040182f5a879985","studentHomeworkId":"26ef84e7689a14e101689a504b173611","title":"PA5(选做) ","url":"[REDACTED]","deadline":"2019-01-06T15:59:59.000Z","submitUrl":"[REDACTED]","submitTime":"2019-01-04T15:46:05.000Z","grade":5,"graderName":"[REDACTED]","gradeContent":"nan","gradeTime":"2019-01-14T20:26:43.000Z","submittedAttachmentUrl":"[REDACTED]","submitted":true,"graded":true,"description":"第五阶段实验编程作业(选做),具体要求可参见附件文件包中的说明(Decaf PA5 README.pdf)","submittedContent":"<!-- <span style=\"line-height: 24px;\"> -->\n\t\t\t\t\t\t\t\t\t\t<span style=\"line-height:2;\">\n\t\t\t\t\t\t\t\t\t\t</span>","attachmentName":"[REDACTED]","attachmentUrl":"[REDACTED]","submittedAttachmentName":"[REDACTED]"}
自我感觉还是比原本的格式好了很多的(其中有一个 nan 的确就是字面值,并不是解析出了问题)。
总结
学校的研发都放假了,我还在写代码,学校应该给我发工资才对。
希望这个库能给大家的学习生活带来一些方便,欢迎随时 PR。下面是一些可能的小目标:
- 支持提交作业、讨论等
- 支持助教/教师功能
- 支持直接下载(吞并 这个项目?)
此外,我正在以此为后端开发支持2018版网络学堂的 Learn Helper,几乎重写了所有的UI和逻辑。预计开学前发布,敬请期待。
thu-learn-lib 开发小记(转)的更多相关文章
- NodeJS+Express+MySQL开发小记(2):服务器部署
http://borninsummer.com/2015/06/17/notes-on-developing-nodejs-webapp/ NodeJS+Express+MySQL开发小记(1)里讲过 ...
- 带农历日历的DatePicker控件!Xamarin控件开发小记
原文:带农历日历的DatePicker控件!Xamarin控件开发小记 闲来无事开发了个日期选择控件,感兴趣的同学前往: https://github.com/MatoApps/Mato.DatePi ...
- Electron Angular 开发小记
一介绍 electron分为主进程和渲染进程,主进程负责和原生交互,控制窗口等. 渲染进程就是普通网页.主进程和渲染进程可以通过ipcMain(主进程使用)及ipcRenderer(渲染进程用)通信 ...
- 微软颜龄Windows Phone版开发小记
随着微软颜龄中文网cn.how-old.net的上线,她也顺势来到了3大移动平台. 用户在微软颜龄这一应用中选择一张包含若干人脸的照片,就可以通过云计算得到他们的性别和年龄. 今天我们就和大家分享一下 ...
- MQTT开发小记(一)
最近在协助公司硬件组进行MQTT协议的嵌入式SDK包开发. 简述一下MQTT MQTT简单的来说是一种订阅/发布模式的通信形式,一般分为客户端和服务器端. MQTT服务器端可以简单理解为一个消息中转站 ...
- POCO C++ lib开发环境构建
Welcome Thank you for downloading the POCO C++ Libraries and welcome to the growing community of POC ...
- Android开发小记
一,下载解压adt-bundle,直接可以用来开发了二,新建android项目时不勾选创建activity,来看看如何手动创建activity1,在空项目添加class文件,选择超类为activity ...
- IOS开发小记-内存管理
关于IOS开发的内存管理的文章已经很多了,因此系统的知识点就不写了,这里我写点平时工作遇到的疑问以及解答做个总结吧,相信也会有人遇到相同的疑问呢,欢迎学习IOS的朋友请加ios技术交流群:190956 ...
- 微信小程序开发小记
年前的时候,因为公司开发小程序的人员不够,临时参与了一个项目中几个小模块的开发,这里做个简单的小记录,眼过千篇不若手过一遍,希望将来如果要用到时不至于大脑空白! 开发工具:wechat_devtool ...
随机推荐
- Android 异步下载
package com.example.demo1; import java.io.File; import java.io.FileOutputStream; import java.io.IOEx ...
- socat管理haproxy以及haproxy调优
Unix套接字命令(Unix Socket commands) socat是一个多功能的网络工具,名字来由是“Socket CAT”,可以看作是netcat的N倍加强版,socat的官方网站:http ...
- Day 08 文件操作模式,文件复制,游标
with open:将文件的释放交给with管理 with open('文件', '模式', encoding='utf-8') as f: # 操作 pass a模式:追加写入 # t ...
- windows下matplotlib编译安装备忘
windows下,codeblocks,mingw安装matplotlib. python下一些源码的编译安装,备忘. matplotlib官网编译好的版本只支持到3.3.我不慎刚下了python3. ...
- SpringBoot配置(2) slf4j&logback
SpringBoot配置(2) slf4j&logback 一.SpringBoot的日志使用 全局常规设置(格式.路径.级别) SpringBoot能自动适配所有的日志,而且底层使用slf4 ...
- Ubuntu 14.10 下Hive配置
1 系统环境 Ubuntu 14.10 JDK-7 Hadoop 2.6.0 2 安装步骤 2.1 下载Hive 我第一次安装的时候,下载的是Hive-1.2.1,配置好之后,总是报错 [ERROR] ...
- Includes() vs indexOf() in JavaScript
碰到一个问题, 部分机器网页数据源不正常, 简单排查发现是使用了较新的Array.includs 方法. 查了下兼容性, chrome 需要47版本以后支持, 客户机果然是很久的43版本. 用Arra ...
- 论气机之"左升右降"
生命现象源于气机的出入升降运动. “出入废则神机化灭,升降息则气立孤危.故非出入,则无以生长壮老已:非升降,则无以生长化收藏”(<素问·六微旨大论>),升降是气机主要的运动形式之一,是 ...
- JavaScript获取、修改CSS样式合辑
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- Spring中AOP主要用来做什么。Spring注入bean的方式。什么是IOC,什么是依赖注入
Spring中主要用到的设计模式有工厂模式和代理模式. IOC:Inversion of Control控制反转,也叫依赖注入,通过 sessionfactory 去注入实例:IOC就是一个生产和管理 ...