python抓取、解析、下載小電影……

老實(shí)說(shuō)是不是因?yàn)闃?biāo)題才點(diǎn)進(jìn)來(lái)的
?雖然我這里沒(méi)有小電影,但是今天的內(nèi)容也是實(shí)打?qū)嵉母韶洠?/span>這叫授人以魚(yú)不如授人以漁,有了這技術(shù),小電影不也是分分鐘的事嘛!
前言
一到周末就想搞點(diǎn)有意思的事,比如之前分享的arduino開(kāi)發(fā),比如上周分享的博客爬蟲(chóng),今天我又想搞點(diǎn)有意思的事,所以就有了今天的內(nèi)容——python爬取m3u8視頻資源。不過(guò)需要在這里需要著重說(shuō)明的是,技術(shù)無(wú)罪,切勿用技術(shù)搞違法犯罪的事,不然日子真的就越來(lái)越有判頭了
。
知識(shí)擴(kuò)展
在開(kāi)始抓取m3u8視頻(小電影)之前,我們先了解下m3u8的相關(guān)知識(shí),了解的更多,可以讓我們少走彎路。
m3u8是什么?
在此之前,我僅僅知道m3u8是一種網(wǎng)絡(luò)串流,在平時(shí)娛樂(lè)時(shí)候會(huì)找一些m3u8的資源,看看直播啥的,直到今天要分享m3u8的相關(guān)內(nèi)容,才真正開(kāi)始搜集m3u8的相關(guān)知識(shí)點(diǎn)。關(guān)于m3u8連百度百科都沒(méi)有說(shuō)明,搜到知乎一篇內(nèi)容(【全網(wǎng)最全】m3u8到底是什么格式?一篇文章搞定m3u8下載),下面的原理圖也是參照的這篇內(nèi)容:

從上面的原理圖中我們可以得到以下知識(shí)點(diǎn):
首先 m3u8并非是視頻格式,而是視頻文件的索引。ts文件才是我們真正播放的視頻資源。ts是日本高清攝像機(jī)拍攝下進(jìn)行的封裝格式,全稱(chēng)為MPEG2-TS。ts即"Transport Stream"的縮寫(xiě)。MPEG2-TS格式的特點(diǎn)就是要求從視頻流的任一片段開(kāi)始都是可以獨(dú)立解碼的。
m3u8通常分兩種格式,一種是單碼率(固定分辨率),一種是多碼率(包含多種分別率)。下面就是一個(gè)單碼率的m3u8文件的內(nèi)容:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:1619459525
#EXT-X-PROGRAM-DATE-TIME:2021-10-23T05:39:42Z
#EXTINF:10.0,
1634967582-1-1619459525.hls.ts
#EXT-X-PROGRAM-DATE-TIME:2021-10-23T05:39:52Z
#EXTINF:10.0,
1634967592-1-1619459526.hls.ts
#EXT-X-PROGRAM-DATE-TIME:2021-10-23T05:40:02Z
#EXTINF:10.0,
1634967602-1-1619459527.hls.ts
這個(gè)就是一個(gè)多碼率的m3u8文件,從多碼率的文件格式可以看出來(lái),多碼率中包括了多個(gè)單碼率是m3u8文鏈接:
#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=500000,RESOLUTION=480x270
500kb/hls/index.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=300000,RESOLUTION=360x202
300kb/hls/index.m3u8
m3u8文件指令
另外我在另外一篇博客發(fā)現(xiàn)m3u8其實(shí)是m3u文件的擴(kuò)展(參考文檔2),這可能就是為什么沒(méi)有找到m3u8相關(guān)詞條的原因吧,同時(shí)在m3u的詞條中發(fā)現(xiàn)了M#U文件的指令描述(每個(gè)字段的含義):
#EXTM3U?//必需,表示一個(gè)擴(kuò)展的m3u文件
#EXT-X-VERSION:3?//hls的協(xié)議版本號(hào),暗示媒體流的兼容性
#EXT-X-MEDIA-SEQUENCE:xx?//首個(gè)分段的sequence?number
#EXT-X-ALLOW-CACHE:NO?//是否緩存
#EXT-X-TARGETDURATION:5?//每個(gè)視頻分段最大的時(shí)長(zhǎng)(單位秒)
#EXT-X-DISCONTINUITY?//表示換編碼
#EXTINF:?//每個(gè)切片的時(shí)長(zhǎng)
另外關(guān)于m3u的文件指令是有國(guó)際標(biāo)準(zhǔn)的,感興趣的小伙伴可以去看下:
http://tools.ietf.org/html/draft-pantos-http-live-streaming-06
好了,關(guān)于m3u8的相關(guān)內(nèi)容,我們就說(shuō)這么多,感興趣的小伙伴可以自己繼續(xù)探索,下面我們看下如何用python抓取、解析和下載m3u8文件索引中的視頻文。
抓取、解析、下載
首先我們先要拿到目標(biāo)視頻資源的索引文件,也就是m3u8文件。一般稍微懂點(diǎn)web開(kāi)發(fā)的小伙伴,應(yīng)該都知道瀏覽器抓包吧,F12打開(kāi)瀏覽器控制臺(tái),然后選擇Network,刷新下頁(yè)面,在左側(cè)資源區(qū)找到index.m3u8文件。
通常會(huì)有兩個(gè)m3u8,第一個(gè)是獲取視頻碼率列表的,也就是多碼率m3u8,這個(gè)文件我們是沒(méi)辦法直接解析的,我們要找的是包含ts視頻資源的m3u8文件。這里我隨便在網(wǎng)上搜了一個(gè)葫蘆娃的視頻,然后通過(guò)控制臺(tái)拿到m3u8文件地址:

https://vod1.bdzybf1.com/20200819/wMgIH6RN/1000kb/hls/index.m3u8
下面我們就用python解析下載這個(gè)視頻文件。
解析m3u8文件
解析m3u8文件最核心的地方是分析m3u8的文件結(jié)構(gòu),然后根據(jù)其文件內(nèi)容寫(xiě)出對(duì)應(yīng)的解析邏輯。這里我推薦直接用requests庫(kù)模擬調(diào)用,然后分析響應(yīng)結(jié)果,因?yàn)橛梦谋竟ぞ咧?lèi)的查看m3u8文件的話,換行符\n、制表符\t這些看起來(lái)不夠直觀,但是requests就不會(huì)有這個(gè)問(wèn)題,因?yàn)槲覀兘馕龅木褪?code style="overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(255, 100, 65);">requests的響應(yīng)結(jié)果。
這里的以我們上面m3u8文件為例響應(yīng)結(jié)果如下:

可以清晰地看出,這個(gè)m3u8文件是通過(guò)換行符\n分割的,有部分m3u8文件中會(huì)出現(xiàn)制表符和換行符組合的情況,所以具體情況具體分析。
另外,我從響應(yīng)內(nèi)容中發(fā)現(xiàn),這個(gè)視頻資源是進(jìn)行了AES-128加密的,所以后面在下載視頻資源的時(shí)候要解密。
解析ts視頻地址
因?yàn)?code style="overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(255, 100, 65);">python代碼都很簡(jiǎn)單,代碼量也不多,所以就不展開(kāi)講了,看代碼注釋?xiě)?yīng)該可以看懂。這個(gè)方法主要是為了獲取ts視頻文件的地址。
import?requests
def?getTsFileUrlList(m3u8Url):
????#?組裝請(qǐng)求頭
????headers?=?{
????????????'User-Agent':?'Mozilla/5.0?(Windows?NT?10.0;?Win64;?x64)?AppleWebKit/537.36?(KHTML,?like?Gecko)?Chrome/75.0.3770.100?Safari/537.36'
????????????}
????#?請(qǐng)求?m3u8文件,并拿到文件內(nèi)容
????ts_rs?=?requests.get(url?=?m3u8Url,?headers=headers).text
????print(ts_rs)
????#?換行符分割文件內(nèi)容
????list_content?=?ts_rs.split('\n');
????print('list_content:{}'.format(list_content))
????player_list?=?[]
????#?循環(huán)分割結(jié)果
????for?line?in?list_content:
??????#?以下拼接方式可能會(huì)根據(jù)自己的需求進(jìn)行改動(dòng)
??????if?'#EXTINF'?in?line:
????????continue;
??????#?僅記錄?ts文件地址??
??????elif?line.endswith('.ts'):
????????player_list.append(line)
????print('數(shù)據(jù)列表組裝完成-size:?{}'.format(len(player_list)));
????return?player_list;
運(yùn)行上面這個(gè)方法的話,最終會(huì)獲取到m3u8索引文件中所有的ts視頻地址,但是由于我們剛才已經(jīng)從m3u8文件中發(fā)現(xiàn)了,視頻資源是加密的,所以在下載的時(shí)候我們需要解密。
下載文件
這里的方法是下載并解密視頻資源,這里我加了一個(gè)解密操作,因?yàn)樽⑨寜蚯逦晕乙膊贿^(guò)多贅述了
def?fileDownloadWithdecrypt(fileSavePath,?player_list):
????#?創(chuàng)建文件夾
????if?not?os.path.exists(fileSavePath):
????????os.mkdir(fileSavePath);
????#?循環(huán)?ts?視頻資源列表
????for?index,?url?in?enumerate(player_list):
????????#?發(fā)送下載請(qǐng)求
????????ts_video?=?requests.get(url?=?url,?headers=headers)
????????#?保存文件
????????with?open('{}/{}.ts'.format(fileSavePath,?str(index?+?1)),?'wb')?as?file:
????????????#?視頻資源解密
????????????context?=?decrypt(ts_video.content);
????????????#?文件寫(xiě)入
????????????file.write(context)
????????????print('正在寫(xiě)入第{}個(gè)文件'.format(index?+?1))
????print('下載完成');
下面是解密代碼
from?Crypto.Cipher?import?AES???#?用于AES解碼
def?decrypt(context):
????#?加密的key
????key?=??b'2cd1da2aedacaec8';
????#?解密
????cryptor?=?AES.new(key,?AES.MODE_CBC,?key);
????decrypt_content?=?cryptor.decrypt(context);
????return?decrypt_content;
這里用的是pycryptodome庫(kù),安裝方式如下:
?pip3?install?pycryptodome
關(guān)于密文,在m3u8文件里面已經(jīng)有了,直接下載就可以拿到,這里我就不通過(guò)代碼拿了:

這里下載視頻會(huì)比較費(fèi)時(shí)間,為了提高下效率可以用多線程,但是由于時(shí)間的關(guān)系就不演示了。
合并視頻
合并視頻就更簡(jiǎn)單了,就是講前面我們保存的視頻合并成一個(gè)完整的視頻,合并完之后的格式是mp4。
def?fileMerge(filePath):
????#?查詢(xún)出文件中的ts文件
????c?=?os.listdir(filePath)
????#?打開(kāi)視頻保存文件
????with?open('%s.mp4'?%?filePath,?'wb+')?as?f:
??????#?循環(huán)
??????for?i?in?range(len(c)):
????????#?打開(kāi)?ts?視頻文件
????????x?=?open('{}/{}.ts'.format(filePath,?str(i?+?1)),?'rb').read()
????????f.write(x)
????print('合并完成')
我看了下,短短的一集葫蘆娃,總共被分割成272個(gè)ts文件(好像比這個(gè)多,我沒(méi)下載完就把網(wǎng)斷了
):

272個(gè)ts文件合并成一個(gè)視頻文件:

好了,到這里我們python爬取m3u8視頻資源的實(shí)例就結(jié)束了,今天的示例還算比較完美,目標(biāo)也比較完美的達(dá)成了。奈斯!
完整代碼如下:
import?requests
import?os
from?Crypto.Cipher?import?AES???#?用于AES解碼
headers?=?{
????????????'User-Agent':?'Mozilla/5.0?(Windows?NT?10.0;?Win64;?x64)?AppleWebKit/537.36?(KHTML,?like?Gecko)?Chrome/75.0.3770.100?Safari/537.36'
????????????}
def?getTsFileUrlList(m3u8Url):
????ts_rs?=?requests.get(url?=?m3u8Url,?headers=headers).text
????print(ts_rs)
????list_content?=?ts_rs.split('\n');
????print('list_content:{}'.format(list_content))
????player_list?=?[]
????index?=?1;
????for?line?in?list_content:
??????#?以下拼接方式可能會(huì)根據(jù)自己的需求進(jìn)行改動(dòng)
??????if?'#EXTINF'?in?line:
????????continue;
??????elif?line.endswith('.ts'):
????????player_list.append(line)
????print('數(shù)據(jù)列表組裝完成-size:?{}'.format(len(player_list)));
????return?player_list;????
????
def?fileDownloadWithdecrypt(fileSavePath,?player_list):
????if?not?os.path.exists(fileSavePath):
????????os.mkdir(fileSavePath);
????for?index,?url?in?enumerate(player_list):
????????ts_video?=?requests.get(url?=?url,?headers=headers)
????????with?open('{}/{}.ts'.format(fileSavePath,?str(index?+?1)),?'wb')?as?file:
????????????context?=?decrypt(ts_video.content);
????????????file.write(context)
????????????print('正在寫(xiě)入第{}個(gè)文件'.format(index?+?1))
????print('下載完成');
????
????
def?fileMerge(filePath):
????c?=?os.listdir(filePath)
????with?open('%s.mp4'?%?filePath,?'wb+')?as?f:
??????for?i?in?range(len(c)):
????????x?=?open('{}/{}.ts'.format(filePath,?str(i?+?1)),?'rb').read()
????????f.write(x)
????print('合并完成')
????
????
def?decrypt(context):
????key?=??b'2cd1da2aedacaec8';
????cryptor?=?AES.new(key,?AES.MODE_CBC,?key);
????decrypt_content?=?cryptor.decrypt(context);
????return?decrypt_content;
????
????
if?__name__?==?'__main__':
????m3u8Url?=?'https://vod1.bdzybf1.com/20200819/wMgIH6RN/1000kb/hls/index.m3u8';
????videoList?=?getTsFileUrlList(m3u8Url);
????print(videoList)
????savePath?=?"./test"
????fileDownloadWithdecrypt(savePath,?videoList);
????fileMerge(savePath);
運(yùn)行上面的代碼,你就可以得到一集完整的葫蘆娃
當(dāng)然,如果你能找到資源,用上面這段代碼爬取小電影也是可以的
總結(jié)
今天的視頻爬蟲(chóng)很簡(jiǎn)單,可以說(shuō)非常簡(jiǎn)單,核心技術(shù)點(diǎn)也不多,主要涉及如下幾點(diǎn):
request請(qǐng)求字符串解析(響應(yīng)結(jié)果解析) 文件操作 ASE解密
只需要稍微有一點(diǎn)python基礎(chǔ),就可以做出來(lái),所以這里我也沒(méi)什么好總結(jié)的了。
最后,免費(fèi)為python做一個(gè)無(wú)償廣告。我一直覺(jué)得python是一門(mén)不錯(cuò)的語(yǔ)言,特別是作為腳本使用的時(shí)候,真的是太方便了,今天它也依然沒(méi)有讓我失望。
其實(shí)嚴(yán)格來(lái)說(shuō),我學(xué)python,但是之前一直詬病于它的縮進(jìn)語(yǔ)法,所以也就一直沒(méi)入門(mén),直到做了一段時(shí)間java web開(kāi)發(fā)之后,回頭再看下python,覺(jué)得好簡(jiǎn)單,于是就又愉快地使用它了,不過(guò)用它寫(xiě)腳本真的太爽了,短短幾行代碼,搞定java一個(gè)繁瑣的項(xiàng)目,而且用起來(lái)也很輕便。
最近一年多的時(shí)間,我用它處理過(guò)數(shù)據(jù)、用它跑數(shù)據(jù)庫(kù)統(tǒng)計(jì)過(guò)數(shù)據(jù),然后日程工作中我可以用它生成文件目錄、爬取資料,也感覺(jué)我對(duì)它越來(lái)越有好感,所以在這里強(qiáng)烈安利各位小伙伴都來(lái)學(xué)下python,特別是那些非開(kāi)發(fā)崗位的小伙伴,python簡(jiǎn)直是統(tǒng)計(jì)數(shù)據(jù)的利器,雖然不像廣告吹的那么秀,但是技多不壓身呀,而且它真的是可以極大提供我們的效率。
最后的最后,再?gòu)?qiáng)調(diào)下,技術(shù)無(wú)罪,切勿用技術(shù)搞違法犯罪的事,不然日子真的就越來(lái)越有判頭了!
技術(shù)無(wú)罪,切勿用技術(shù)搞違法犯罪的事,不然日子真的就越來(lái)越有判頭了!
技術(shù)無(wú)罪,切勿用技術(shù)搞違法犯罪的事,不然日子真的就越來(lái)越有判頭了!
重要的事情說(shuō)三遍!!!
另外,今天忙著搞爬蟲(chóng)了,設(shè)計(jì)模式的類(lèi)圖還沒(méi)來(lái)得及補(bǔ),明天要加下油了。好了,鐵子們,晚安吧
參考內(nèi)容
[1] ? ?https://zhuanlan.zhihu.com/p/346683119
[2] ? ?https://www.cnblogs.com/shakin/p/3870439.html
