近日某有一友张三,向我吐槽,有一视频网站只能在线观看,不能下载,体验很差。
我当即决定助人为乐,前去一探究竟!
分析网页
打开其中一个视频网页,https://???.?????.com/?/??e/p/279/2?8/69???21.html
控制台查看网络
原来是m3u8格式的视频。虽然没有学习过,但本着助人为乐的精神,我决定给好友写一个视频下载器。
m3u8格式是utf-8格式的m3u文件,m3u文件是记录了一个按索引排序的多个.ts视频片段的文件。也就是将一个完整视频,先拆分成多个.ts视频片段,然后把这些.ts视频片段以地址形式存放进.m3u8的文件里。
既然是m3u8格式,重要的当然是m3u8文件的获取。这里有两个m3u8文件,检查两个index.m3u8文件
第一个:
https://????.???.xyz/v/71f97????18ec9080a2/index.m3u8
#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=3000000,RESOLUTION=720x406
3000kb/hls/index.m3u8
第二个:
https://????.???.xyz/v/71f97????18ec9080a2/3000kb/hls/index.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-KEY:METHOD=AES-128,URI="key.key"
#EXTINF:5,
l3VymExG.ts
#EXTINF:5,
4RfV0Lvj.ts
#EXTINF:0.08,
RVRxCjSO.ts
#EXT-X-ENDLIST
显然,第一个m3u8记录了真正的视频文件的地址,
第二个文件才是我们想要的,记录了该视频所有ts文件名和视频切片长度。
观察网页源码,发现视频控件通过iframe
嵌入,其src
属性包含了我们想要的文件地址
<iframe src="images/m3u8/?url=https://????.???.xyz/v/71f97????18ec9080a2/index.m3u8" frameborder="0" allowtransparency="true" scrolling="YES" width="97%" height="340"></iframe>
上面的地址指向一个video.js
来控制视频下载和播放,我们只需要其中m3u8的url。
因此给定一个网页,就可以得到视频的真实m3u8文件,从而下载视频。
此时下载到的视频是无法观看的,因为被加密了。
观察第二个m3u8文件内容,发现其中说明了视频的加密方式:
#EXT-X-KEY:METHOD=AES-128,URI="key.key"
也就是使用AES-128加密,密钥记录在key.key
文件中
打开一个key.key
文件,得到16位密钥:
d9a772b4cbee99ba
这里并没有给出偏移量iv,猜测默认为b'0000000000000000'
。
至此得到了下载视频的完整流程:
获取m3u8->获取真实m3u8->下载视频
python代码
使用pycryptodome
库进行AES解密
完整代码如下:
import requests
from bs4 import BeautifulSoup as bs
from Crypto.Cipher import AES
import re
import os
import time
global url1, key, lists
def get_m3u8(url, headers={}): # 获取m3u8url
global url1, key
print("获取m3u8文件...")
url = re.findall("http[0-9A-Za-z.:/]{1,100}.m3u8", str(bs(requests.get(url, headers).content, "lxml").find(name="iframe")))[0]
print(url)
url = re.findall('https[0-9A-Za-z.:/]{1,100}(?=index.m3u8)', url)[0] + re.findall('[0-9]{1,10}kb[0-9a-zA-Z./]{1,100}.m3u8', str(bs(requests.get(url, headers={}).content, "lxml")))[0]
print(url)
url1 = re.findall('https[0-9A-Za-z.:/]{1,100}(?=index.m3u8)', url)[0] # 用于m3u8列表合并
key = requests.get(url1 + "key.key", headers={}).text
return url
def get(url, headers={}): # 返回m3u8内容
return bs(requests.get(url, headers).content, "lxml")
def extract(text): # 提取m3u8文件列表
global lists
print("提取m3u8列表...")
lists = re.findall("[0-9A-Za-z]{1,10}.ts", str(text))
return lists
def join_url(lists): # 拼接url
lists1 = ["6"] * len(lists)
for i in range(len(lists)):
lists1[i] = url1 + lists[i]
return lists1
def download(lists, headers={}): # 下载ts文件
lists_url = join_url(lists)
if not os.path.exists("./ts"):
os.makedirs("./ts")
print("开始下载文件...")
for i in range(len(lists)):
print("正在下载:", lists[i], "(", str(i + 1), "/", str(len(lists)), ")")
with open("./ts/" + lists[i], "wb") as f:
f.write(decrypt(requests.get(lists_url[i], headers).content, key))
time.sleep(1)
def decrypt(content, keys, iv=b'0000000000000000'): # AES解密
cipher = AES.new(keys.encode('utf-8'), AES.MODE_CBC, iv)
return cipher.decrypt(content)
def merge(filename): # 合并ts文件
global lists
print("开始合并文件...")
f = open(filename + ".ts", "wb")
for names in lists:
with open("./ts/" + names, "rb") as f1:
f.write(f1.read())
f.close()
print("合并完毕!")
def remove(): # 删除ts文件
pass
if __name__ == '__main__':
urls = "https://???.?????.com/?/??e/p/279/2?8/69???21.html"
download(extract(get(get_m3u8(urls))))
merge("测试")
经过测试,下载得到了完整的视频文件。但是友人张三却道不好用,原来该网站视频通过不同cdn存储资源,每个视频m3u8都不一样,m3u8内容也十分奇怪,只能手动写每个视频的下载。我只能拍拍张三,劝其放下欲望,不如锻炼体魄(笑
总结
第一次接触m3u8格式的视频,终于知道论坛大佬们下载器的原理!(又为自己学到新知识而感到高兴)希望我的代码能给大家带来帮助(不是用来下载某种视频嗷
所写代码其中不足还请大佬们指点!