基于JS的教学立方课件下载脚本

观前提醒

NOTE: 脚本仅作学习研究之用, 敬请尊重版权和他人劳动成果

由 GreasyFork 上的脚本改写而来, 因教学立方网站结构改动, 原脚本已失效. 本脚本在其基础上做了适当的修改以适用于新的网站结构.

此脚本依 MIT 协议开源于 GitHub:

LITS/teaching-applysquare at master · ricky9w/LITS (github.com)

需求分析

网页加载完成后出现一个”显示下载链接”按钮, 点击按钮后所有课件后方出现”下载”按钮, 点击即可下载课件, 无论课件本身是否允许下载.

网站分析

原先教学立方网站通过 HTML 加载, 没给下载权限的课件其地址也在 HTML 文档中, 简单分析文档提取文件 URL 即可. 但是网站改版后使用了阿里云 OSS 服务, 并将所有内容改为动态加载, 课件的标题路径等都通过 XHR 加载, 因此需要通过脚本请求相应 XHR 文件并解析出文件路径.

首先到课件列表进行抓包, 发现有一个如下请求:

1
https://teaching.applysquare.com/Api/CourseAttachment/getList/token/9f448892e12240b40d8c97b80931f8ecsYSOq4CLrtuGp6jetM_YypuKnKCBndPNgYx4aq54iaCwc39hlo-jzoLN0s3Kk7vKgJynnppmptCUpGypu3d5ba2db1IOPo63FfHKfuod_qISkrsuftKCh?parent_id=0&page=1&plan_id=-1&uid=123456&cid=12345

URL 中的 token 后面一段字母+数字组合是每次登陆上去之后网站自动生成的 token, 重点需要关注的是几个 String Parameters , 含义如下:

  • parent_id: 设置为 0 即可
  • page: 当前处于课件列表第几页, 可以通过分析 HTML 文档元素获取
  • plan_id: 用途不明, 可以用 lessonindex.plan_id 获取
  • uid: 用户 id, 可以使用 lessonindex.uid 获取
  • cid: 课程 id, 每个课程不一样, 可以用 lessonindex.cid 获取

了解如上信息如何获取之后就可以构造请求来获取当前所在页面的课件列表. 例如:

1
2
3
4
5
6
7
8
9
10
var page_index = document.getElementsByClassName('pagination')[0];
if (page_index == undefined) {
page_index = 1;
} else {
page_index = page_index.getElementsByClassName('active')[0].children[0].innerText;
}
var list_data = { parent_id: 0, page: page_index, plan_id: lessonindex.plan_id, uid: lessonindex.uid, cid: lessonindex.cid };
$.get('/Api/CourseAttachment/getList' + top_controller.$apendUrl(), top_controller.$appendParams(list_data), function (res) {
/*process list info*/
}

该请求的 response 中包含了当前页面中课件的标题, 发布时间等信息, 组织成了一个 list. 某项课件的相关信息是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
attach_id: "1234567"
can_download: "0"
ext: "mp4"
file_type: "音视频"
id: "123456"
is_auto_publish: "0"
is_read: 0
path: ""
publish_status: "published"
publish_time: "1970-01-01 08:00:00"
publish_user: "发布者"
size: "123.45 MB"
tag_count: 0
title: "这是一个课件.mp4"
transcoding: "1"

如果 can_download 字段为 0, 则 path 属性为空, 反之则会显示课件的真实下载地址. 剩下的任务就是获取那些没有下载权限的课件, 获取其地址.

随便点开一个没有下载权限的课件并抓包, 发现有一个这样的请求:

1
https://teaching.applysquare.com/Api/CourseAttachment/ajaxGetInfo/token/9f448892e12240b40d8c97b80931f8ecsYSOq4CLrtuGpq54iaCwc39hlo-jzoLN0s3Kk7vKgJynnppmptCUpGypu3d5ba2dbqCKaZbIhqeolre20dWZeYZrmnjI1IOPo63FfHKfuod_qISkrsuftKCh?id=123456&cid=12345&uid=54321

请求 URL 中大部分参数在上面已经分析过了, 只剩下一个 id 字段. 每个课件的 id 都不一样, 而这一字段其实是包含在上面获得的列表当中的. 因此对上面的列表进行分析, 提取出每个课件的 id, 构造请求, 即可获得含有该课件 path 的 response, 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
attach_id: "1234567"
attach_type: 1
attr: []
can_download: "0"
cid: "12345"
create_at: "1970-01-01 08:00:00"
download_percent: 0
ext: "mp4"
filename: "filename"
id: "123456"
is_auto_publish: "0"
is_new: "1"
page_count: "0"
path: "path-of-item"
plan_id: "0"
publish_status: 1
publish_time: "1970-01-01 08:00:00"
read_percent: 2.44
real_name: "real-name"
size: "123.45 MB"
status: "1"
title: "title"
transcoding: "1"
transcoding_path: ["",…]
vframe: ""
video_length: "7312"
status: 10001

关注上面的 path 字段, 是阿里云 OSS 提供的数据接口. String Parameters 中含有 OSSAccessKeyId , Expires 以及 Signature 三个字段, 看上去很唬人, 但是不加也不会影响下载(至少目前是这样).

具体实现

有了如上分析, 脚本的实现步骤已经很清楚了, 大致如下:

  1. 构造请求获取当前页面上的课件信息列表
  2. 分析返回的列表获取每个课件的 id 信息
  3. 对每个课件构造请求获取其具体信息, 从中提取课件真实路径
  4. 在页面中对应位置显示下载按钮并链接到课件 URL

细节补充

上面已经给出了脚本的实现步骤, 结合一定 JavaScript 知识很容易实现上述逻辑. 但是有两个细节还需特别关注:

  • 本来已经开放下载的课件无需重复处理

    解决方法也很简单, 本来已经提供下载的课件其 “操作” 栏中有两个按钮: 查看下载 , 未开放下载链接的课件则只有 查看 一种操作. 根据页面中对应元素的个数即可判断课件是否开放下载

  • 同步与异步处理问题

    开始写脚本的时候因为刚接触 JavaScript(其实从来没写过也根本没学过), 只是看过一些脚本依葫芦画瓢会一点基础语法, 因此不知道 JavaScript 语句默认以异步方式执行, 这就导致了一些小问题: 本来写了一个循环对每个课件进行处理并在对应位置添加 下载 按钮, 但是由于处理中包含一个 GET 请求, 请求速度比较慢, 因此等获取到请求的时候循环已经执行完了, 这样就把所有下载按钮全部添加到当前页面中最后一个课件上去了. 最危险的是如果我的网络环境不错, 这样的问题根本发现不了.

    学习过 JavaScript 的语言机制之后想到两种解决方案:

    • 使用 $.ajax() 方法并设置 async=false , 这样请求将会同步进行, 避免出现问题. 简单粗暴, 但是性能差了点
    • 稍微修改代码结构使得脚本可以正确地异步执行. 详细结构可以参考最终代码

文末总结

之前一直用 Python 做爬虫和一些其他用途的自动化脚本, 对 JavaScript 接触很少. 但是使用 JavaScript 配合 Tampermonkey 插件很多事情做起来还是很方便的, 毕竟浏览器已经提供了一个非常好的平台, 很多事情并不需要重头再来.

由于本人才疏学浅, 加上初次接触 JavaScript, 以上分析以及项目脚本中难免有疏漏以及不足, 欢迎交流指正.