python爬马士兵教育视频

因周五,在等放假,刚好朋友买了套马士兵的MCA课,让我帮忙爬下来,因ts使用AES加密,大概耗时2小时写完

1.获取课程列表接口

这个就很简单了, 直接打开chrome的开发者工具, 查看Fetch/XHR, 同时有多个异步请求, 根据返回值可以看到以下接口

https://gateway.mashibing.com/edu-course/systemCourse/child/课程id

此接口返回值的stageList为课程分类id,需要携带token,token格式为Bearer, 封装个网络请求方便后面使用

class HttpRequests:

    def __init__(self, token=None):
        self.s = requests.Session()
        self.s.headers = {"Content-Type": "application/json"}
        if token:
            self.s.headers.update({"Authorization": token})
    def send_req(self, method, url, data=None, token=None, **kwargs):
        if method.upper() == "GET":
            resp = self.s.get(url, params=data, **kwargs)
        else:
            resp = self.s.request(method, url, data=data, **kwargs)
        return resp

使用上面封装的网络请求进行获取列表数据

token = "Bearer ********"

# 下面为数据返回格式
eduCourse = {
    "id": 0,
    "title": "",
    "stageList": [
        {
            "id": "22",
            "title": "导学篇",
            "description": "导学篇",
            "courseList": [
                {
                    "id": 910,
                    "courseName": "导学篇(必看)",
                }
            ]
        }
    ],
}


def getCourseList(id):
    url = "https://gateway.mashibing.com/edu-course/systemCourse/child/{}".format(id)
    list = HttpRequests.HttpRequests(token=token).send_req("get", url).json()
    eduCourse["id"] = list["data"]["id"]
    eduCourse["title"] = list["data"]["title"]
    eduCourse["stageList"] = list["data"]["stageList"]

 title=

2.获取课程视频地址

获取到课程列表后我们下一步肯定是要获取视频的地址了,然后把视频保存到本地,这时我们随便点进去一个分类可以看到页面没有刷新,也是通过Fetch/XHR方式获取的分类下课程的数据, 我们继续打开浏览器的开发者工具,可以看到接口

https://gateway.mashibing.com/edu-course/courseWeb/分类id/pc

根据courseList的id获取视频id, 我们继续写个方法方便后面调用这时候就很简单了

# 根据courseList的id获取视频id
def getVideoId(id):
    url = "https://gateway.mashibing.com/edu-course/courseWeb/{}/pc".format(id)
    return HttpRequests.HttpRequests(token=token).send_req("get", url).json()["data"]

然后可以看到此接口的返回值, 我们以导学篇(必看)为例, id为:910,请求接口地址

https://gateway.mashibing.com/edu-course/courseWeb/910/pc

可以看到返回值data里内容为 (为缩短文章,无用返回值已删除)

{
    "data": {
        "courseNo": 910,
        "courseName": "导学篇(必看)",
        "detailDesc": "如何在平台上学习?如何看课学习?如何提问?如何进行代码仓库操作?这些都是进入体系化学习前的第一课",
        "chapterList": [
            {
                "courseNo": 910,
                "chapterNo": 10312,
                "chapterName": "平台功能介绍",
                "chapterSequence": 1,
                "chapterCount": 1,
                "chapterDurationTimeCount": 115,
                "isFinish": false,
                "sectionList": [
                    {
                        "relCourseNo": 910,
                        "relChapterNo": 10312,
                        "sectionNo": 34548,
                        "sectionName": "平台功能介绍(Web端)",
                        "durationTime": 115,
                        "sectionSequence": 1,
                        "isTrial": false,
                        "isStudy": true,
                        "studyCount": null,
                        "updating": 0,
                        "hasExam": null,
                        "dataUrl": null,
                        "sectionType": 1,
                        "ployvVideoId": "4ffae39b72614e895673b9484cfb9043_4",
                        "studyState": 2,
                        "definedUpdateTime": null,
                        "liveRecordId": null,
                        "downloadVideoSize": null,
                        "studentCourseWorkList": []
                    }
                ]
            },
        ],
    }
}

这时候我们可以拿到该分类的每个视频信息了

3.获取视频下载地址

https://hls.videocc.net/4ffae39b72/9/4ffae39b72614e895673b9484cfb9043.m3u8

这时候我们继续点进去一个视频可以看到浏览器的开发者工具里又一个.m3u8结尾的地址,前面内容则是上面接口返回的ployvVideoId去掉_4

这时候我们需要下载查看这个m3u8内容, 发现此m3u8内容如下

#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=800000, NAME="720p HD"
4ffae39b72614e895673b9484cfb9043_3.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=512000, NAME="360p SD"
4ffae39b72614e895673b9484cfb9043_2.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=256000, NAME="270p 3G"
4ffae39b72614e895673b9484cfb9043_1.m3u8

很明显此文件为根据清晰度进行切换的,我们下载肯定是需要最清晰的清晰度,所以我们选择比特率最高的,也就是第三行的,我们再写一个函数方便后面调用

# 最清晰的视频m3u8地址
def getVideoList(videoId):
    url = "https://hls.videocc.net/4ffae39b72/9/{}.m3u8".format(videoId[:-2])
    t = HttpRequests.HttpRequests(token=token).send_req("get", url).text
    return t.split("\n")[2][-6:]

此时我们已经拿到了视频最清晰的m3u8地址, 我们下载后打开查看,发现内容为

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:YES
#EXT-X-KEY:METHOD=AES-128,URI="https://hls.videocc.net/4ffae39b72/3/4ffae39b72614e895673b9484cfb9043_3.key",IV=0x018b729047067d91aa4a3120a76cfbc5
#EXT-X-TARGETDURATION:11
#EXTINF:8.400000,
https://hw-mts.videocc.net/4ffae39b72/0/1234567890123/3/fb/90/43_3/4ffae39b72614e895673b9484cfb9043_3_0.ts
#EXTINF:1.666667,
https://hw-mts.videocc.net/4ffae39b72/0/1234567890123/3/fb/90/43_3/4ffae39b72614e895673b9484cfb9043_3_1.ts
#EXTINF:10.000000,
https://hw-mts.videocc.net/4ffae39b72/0/1234567890123/3/fb/90/43_3/4ffae39b72614e895673b9484cfb9043_3_2.ts
#EXTINF:10.000000,
https://hw-mts.videocc.net/4ffae39b72/0/1234567890123/3/fb/90/43_3/4ffae39b72614e895673b9484cfb9043_3_3.ts
#EXTINF:10.000000,
https://hw-mts.videocc.net/4ffae39b72/0/1234567890123/3/fb/90/43_3/4ffae39b72614e895673b9484cfb9043_3_4.ts
#EXTINF:10.000000,
https://hw-mts.videocc.net/4ffae39b72/0/1234567890123/3/fb/90/43_3/4ffae39b72614e895673b9484cfb9043_3_5.ts
#EXTINF:10.000000,
https://hw-mts.videocc.net/4ffae39b72/0/1234567890123/3/fb/90/43_3/4ffae39b72614e895673b9484cfb9043_3_6.ts
#EXTINF:10.000000,
https://hw-mts.videocc.net/4ffae39b72/0/1234567890123/3/fb/90/43_3/4ffae39b72614e895673b9484cfb9043_3_7.ts
#EXTINF:10.000000,
https://hw-mts.videocc.net/4ffae39b72/0/1234567890123/3/fb/90/43_3/4ffae39b72614e895673b9484cfb9043_3_8.ts
#EXTINF:10.000000,
https://hw-mts.videocc.net/4ffae39b72/0/1234567890123/3/fb/90/43_3/4ffae39b72614e895673b9484cfb9043_3_9.ts
#EXTINF:10.000000,
https://hw-mts.videocc.net/4ffae39b72/0/1234567890123/3/fb/90/43_3/4ffae39b72614e895673b9484cfb9043_3_10.ts
#EXTINF:10.000000,
https://hw-mts.videocc.net/4ffae39b72/0/1234567890123/3/fb/90/43_3/4ffae39b72614e895673b9484cfb9043_3_11.ts
#EXTINF:5.000000,
https://hw-mts.videocc.net/4ffae39b72/0/1234567890123/3/fb/90/43_3/4ffae39b72614e895673b9484cfb9043_3_12.ts
#EXT-X-ENDLIST

4.视频m3u8分析

可以看到,ts是进行加密的,那么既然浏览器端可以查看就代表解密在前端,我们继续看浏览器开发者工具的网络请求可以发现https://hls.videocc.net/4ffae39b72/3/4ffae39b72614e895673b9484cfb9043_3.key这个地址后面有个参数?token=xxx

那么我们只需要拿到这个参数进行请求即可获取到解密的key,继续查看请求的接口查看是哪个接口返回了这个token,最后发现

https://gateway.mashibing.com/msb-video/ployv/playerSafe

上面的接口post传了videoId,后返回了解密的key 我们再封装一个函数负责获取对应视频的key

def getPlayerSafe(videoId):
    url = "https://gateway.mashibing.com/msb-video/ployv/playerSafe"
    return HttpRequests.HttpRequests(token=token).send_req("post", url, json.dumps({"videoId": videoId})).json()["data"]

这时候我们可以把ts下载下来进行解密了,首先肯定需要解析这个m3u8文件获取所有的ts下载地址,我们继续写个函数方便调用

import m3u8

# 视频M3U8
def getM3U8(videoId, t):
    url = "https://hls.videocc.net/4ffae39b72/9/{}_{}".format(videoId[:-2], t)
    m3u8_obj = m3u8.load(url)
    iv = re.search('IV=(.*)', HttpRequests.HttpRequests(token=token).send_req("get", url).text).group(1).replace('0x',
                                                                                                                 "")[
         :16].encode()
    key = m3u8_obj.files[0]
    files = m3u8_obj.files[1:]
    return {
        "key": key,
        "ts": files,
        "iv": iv
    }

我们这时候已经拿到了所有的ts地址,kev的地址(未携带token)和iv,这时候我们只需要把ts下载到本地进行解密后把视频合并为一个文件即可

5.视频ts解密

我们使用python的Crypto来进行解密AES 解密部分大概如下

from Crypto.Cipher import AES
key = "{}?token={}".format(videoData["key"], getPlayerSafe(videoItem2["ployvVideoId"])["playSafe"])
crypto = AES.new(HttpRequests.HttpRequests(token=token).send_req("get", key).content, AES.MODE_CBC,videoData["iv"])
with open('{}.ts'.format(name), 'wb') as f:
    for chunk in r.iter_content(chunk_size=1024):
        if chunk:
            f.write(crypto.decrypt(chunk))

我们知道每个ts都是一个很短的视频,我们需要对ts进行合成为一个文件

6.ts合成为mp4

windows

在windows下可以使用copy /b命令对ts进行合成

ts_list = glob.glob('*.ts')
# ts_list 排序
ts_list = sorted(ts_list, key=lambda i: int(re.search('(\d+).ts', i).group(1)))
# 合并命令
merge_ts_list = "+".join(ts_list)
merge_command = f'copy /b {merge_ts_list} {videoItem2["sectionName"]}.mp4>tmp'
# 执行合并命令
os.system(merge_command)

linux

在linux下可以使用cat 命令对ts进行合成

ts_list = glob.glob('*.ts')
# ts_list 排序
ts_list = sorted(ts_list, key=lambda i: int(re.search('(\d+).ts', i).group(1)))
# 合并命令
merge_ts_list = " ".join(ts_list)
merge_command = f'cat {merge_ts_list} >> {videoItem2["sectionName"]}.mp4'
# 执行合并命令
os.system(merge_command)
# 删除ts
for _ in glob.glob('*.ts'):
    os.remove(_)

这时我们执行,可以看到视频如我们所愿进行下载且合并
 title=