Python爬取視訊之愛情電影及解密TS檔案和兩種合併ts!
俗話說,興趣所在,方能大展拳腳。so結合興趣的學習才能事半功倍,更加努力專心,apparently本次任務是在視訊網站爬取一些好看的小電影,地址不放(狗頭保命)只記錄過程。
實現功能:
從網站上爬取採用m3u8分段方式的視訊檔案,對加密的"ts"檔案解密,實現兩種方式合併"ts"檔案,為防止IP被封,使用代理,最後刪除臨時檔案。
環境 &依賴
- Win10 64bit
- IDE:Pycharm
- Python 3.8
- Python-site-package:requests +BeautifulSoup + lxml + m3u8 + AES
在PyCharm中建立一個專案會建立一個臨時目錄存放環境和所需要的package包,所以要在PyCharm中專案直譯器(ProjectInterpreter)中新增所有需要的包,這張截圖是本專案的包列表,紅框中是所必須的包,其他有的包我也不知道做什麼用的。
下面開始我們的正餐,爬取資料第一步我們需要解析目標網站,找到我們需要爬取視訊的地址,F12開啟開發者工具
很不幸,這個網站視訊是經過包裝採用m3u8視訊分段方式載入
科普一下:m3u8 檔案實質是一個播放列表(playlist),其可能是一個媒體播放列表(Media Playlist),或者是一個主列表(Master Playlist)。但無論是哪種播放列表,其內部文字使用的都是 utf-8 編碼。
當 m3u8 檔案作為媒體播放列表(Meida Playlist)時,其內部資訊記錄的是一系列媒體片段資源,順序播放該片段資源,即可完整展示多媒體資源。
OK,本著“沒有解決不了的困難“的原則我們繼續,依舊在開發者模式,從Elements模式切換到NetWork模式,去掉不需要的資料,我們發現了兩個m3u8檔案一個key檔案和一個ts檔案
分別點選之後我們可以看到對應的地址
OK,現在地址已經拿到了,我們可以開始我們的資料下載之路了。
首先進行初始化,包括路徑設定,請求頭的偽裝等,之後我們通過迴圈去下載所有ts檔案,至於如何定義迴圈的次數我們可以通過將m3u8檔案下載之後解析檔案得到所有ts的列表,之後拼接地址然後迴圈就可以得到所有ts檔案了。
觀察資料,不是真正路徑,第二層路徑在第三行可以看到,結合我們對網站原始碼分析再次拼接字串請求:
之後我們迴圈得到的TS列表,通過拼接地址下載視訊片段。但是問題遠遠沒有這麼簡單,我們下載的ts檔案居然無法播放,通過對第二層下載得到的m3u8檔案進行分析我們可以發現這一行程式碼:
#EXT-X-KEY:METHOD=AES-128,URI="key.key"
此網站採用AES方法對所有ts檔案進行了加密,其中
METHOD=ASE-128 :說明此視訊採用ASE-128方式進行加密,
URI=“key.key”:代表key的地址
綜上所訴,感覺好難啊,好繞了,都拿到了視訊還看不了,但是我們要堅持我們的初心不能放棄。Fortunately,我們應該慶幸Python強大的模組功能,這個問題我們可以通過下載AES模組解決。
完成之後我們需要將所有ts合併為一個MP4檔案,最簡單的在CMD命令下我們進入到視訊所在路徑然後執行:
copy /b *.ts fileName.mp4
需要注意所有TS檔案需要按順序排好。在本專案中我們使用os模組直接進行合併和刪除臨時ts檔案操作。
完整程式碼:之後我們迴圈得到的TS列表,通過拼接地址下載視訊片段。但是問題遠遠沒有這麼簡單,我們下載的ts檔案居然無法播放,通過對第二層下載得到的m3u8檔案進行分析我們可以發現這一行程式碼:
#EXT-X-KEY:METHOD=AES-128,URI="key.key"
此網站採用AES方法對所有ts檔案進行了加密,其中
METHOD=ASE-128 :說明此視訊採用ASE-128方式進行加密,
URI=“key.key”:代表key的地址
綜上所訴,感覺好難啊,好繞了,都拿到了視訊還看不了,但是我們要堅持我們的初心不能放棄。Fortunately,我們應該慶幸Python強大的模組功能,這個問題我們可以通過下載AES模組解決。
完成之後我們需要將所有ts合併為一個MP4檔案,最簡單的在CMD命令下我們進入到視訊所在路徑然後執行:
copy /b *.ts fileName.mp4
需要注意所有TS檔案需要按順序排好。在本專案中我們使用os模組直接進行合併和刪除臨時ts檔案操作。
完整程式碼:
方法一:
import re
import requests
import m3u8
import time
import os
from bs4 import BeautifulSoup
import json
from Crypto.Cipher import AES
class VideoCrawler():
def __init__(self,url):
super(VideoCrawler, self).__init__()
self.url=url
self.down_path=r"F:\Media\Film\Temp"
self.final_path=r"F:\Media\Film\Final"
self.headers={
'Connection':'Keep-Alive',
'Accept':'text/html,application/xhtml+xml,*/*',
'User-Agent':'Mozilla/5.0 (Linux; U; Android 6.0; zh-CN; MZ-m2 note Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/40.0.2214.89 MZBrowser/6.5.506 UWS/2.10.1.22 Mobile Safari/537.36'
}
def get_url_from_m3u8(self,readAdr):
print("正在解析真實下載地址...")
with open('temp.m3u8','wb') as file:
file.write(requests.get(readAdr).content)
m3u8Obj=m3u8.load('temp.m3u8')
print("解析完成")
return m3u8Obj.segments
def run(self):
print("Start!")
start_time=time.time()
os.chdir(self.down_path)
html=requests.get(self.url).text
bsObj=BeautifulSoup(html,'lxml')
tempStr = bsObj.find(class_="iplays").contents[3].string#通過class查詢存放m3u8地址的元件
firstM3u8Adr=json.loads(tempStr.strip('var player_data='))["url"]#得到第一層m3u8地址
tempArr=firstM3u8Adr.rpartition('/')
realAdr="%s/500kb/hls/%s"%(tempArr[0],tempArr[2])#一定規律下對字串拼接得到第二層地址, 得到真實m3u8下載地址,
key_url="%s/500kb/hls/key.key"%tempArr[0]#分析規律對字串拼接得到key的地址
key=requests.get(key_url).content
fileName=bsObj.find(class_="video-title w100").contents[0].contents[0]#從原始碼中找到視訊名稱的規律
fileName=re.sub(r'[\s,!]','',fileName) #通過正則表示式去掉中文名稱中的感嘆號逗號和空格等特殊字串
cryptor=AES.new(key,AES.MODE_CBC,key)#通過AES對ts進行解密
urlList=self.get_url_from_m3u8(realAdr)
urlRoot=tempArr[0]
i=1
for url in urlList:
resp=requests.get("%s/500kb/hls/%s"%(urlRoot,url.uri),headers=crawler.headers)
if len(key):
with open('clip%s.ts' % i, 'wb') as f:
f.write(cryptor.decrypt(resp.content))
print("正在下載clip%d" % i)
else:
with open('clip%s.ts'%i,'wb') as f:
f.write(resp.content)
print("正在下載clip%d"%i)
i+=1
print("下載完成!總共耗時%d s"%(time.time()-start_time))
print("接下來進行合併......")
os.system('copy/b %s\\*.ts %s\\%s.ts'%(self.down_path,self.final_path,fileName))
print("刪除碎片原始檔......")
files=os.listdir(self.down_path)
for filena in files:
del_file=self.down_path+'\\'+filena
os.remove(del_file)
print("碎片檔案刪除完成")
if __name__=='__main__':
crawler=VideoCrawler("地址大家自己找哦")
crawler.start()
crawler2=VideoCrawler("地址大家自己找哦")
crawler2.start()
方法二
在方法一中我們是下載所有ts片段到本地之後在進行合併,其中有可能順序會亂,有時候解密的視訊還是無法播放合併之後會導致整個視訊時間軸不正確而且視訊根本不能完整播放,在經過各種努力,多方查資料之後有的問題還是得不到完美解決,最後突發奇想,有了一個新的想法,我們不必把所有ts片段都下載到本地之後進行合併,而是採用另一種思維模式,一開始我們只建立一個ts檔案,然後每次迴圈的時候不是去下載ts檔案而是將通過地址得到的視訊片段檔案流直接新增到我們一開始建立的ts檔案中,如果出現錯誤跳出當前迴圈並繼續下次操作,最後我們直接得到的就是一個完整的ts檔案,還不需要去合併所有片段。具體看程式碼如何實現。
本程式碼好多地方和上面都一樣,我們只需要領悟其中的原理和方法就OK了
import re
import requests
import m3u8
import time
import os
from bs4 import BeautifulSoup
import json
from Crypto.Cipher import AES
import sys
import random
class VideoCrawler():
def __init__(self,url):
super(VideoCrawler, self).__init__()
self.url=url
self.down_path=r"F:\Media\Film\Temp"
self.agency_url='https://www.kuaidaili.com/free/' #獲取免費代理的網站,如果網站過期或者失效,自己找代理網站替換
self.final_path=r"F:\Media\Film\Final"
self.headers={
'Connection':'Keep-Alive',
'Accept':'text/html,application/xhtml+xml,*/*',
'User-Agent':'Mozilla/5.0 (Linux; U; Android 6.0; zh-CN; MZ-m2 note Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/40.0.2214.89 MZBrowser/6.5.506 UWS/2.10.1.22 Mobile Safari/537.36'
}
def get_url_from_m3u8(self,readAdr):
print("正在解析真實下載地址...")
with open('temp.m3u8','wb') as file:
file.write(requests.get(readAdr).content)
m3u8Obj=m3u8.load('temp.m3u8')
print("解析完成")
return m3u8Obj.segments
def get_ip_list(self,url, headers):
web_data = requests.get(url, headers=headers).text
soup = BeautifulSoup(web_data, 'lxml')
ips = soup.find_all('tr')
ip_list = []
for i in range(1, len(ips)):
ip_info = ips[i]
tds = ip_info.find_all('td')
ip_list.append(tds[0].text + ':' + tds[1].text)
return ip_list
def get_random_ip(self,ip_list):
proxy_list = []
for ip in ip_list:
proxy_list.append('http://' + ip)
proxy_ip = random.choice(proxy_list)
proxies = {'http': proxy_ip}
return proxies
def run(self):
print("Start!")
start_time=time.time()
self.down_path = r"%s\%s" % (self.down_path, uuid.uuid1())#拼接新的下載地址
if not os.path.exists(self.down_path): #判斷檔案是否存在,不存在則建立
os.mkdir(self.down_path)
html=requests.get(self.url).text
bsObj=BeautifulSoup(html,'lxml')
tempStr = bsObj.find(class_="iplays").contents[3].string#通過class查詢存放m3u8地址的元件
firstM3u8Adr=json.loads(tempStr.strip('var player_data='))["url"]#得到第一層m3u8地址
tempArr=firstM3u8Adr.rpartition('/')
all_content = (requests.get(firstM3u8Adr).text).split('\n')[2]#從第一層m3u8檔案中中找出第二層檔案的的地址
midStr = all_content.split('/')[0]#得到其中有用的字串,這個針對不同的網站採用不同的方法自己尋找其中的規律
realAdr = "%s/%s" % (tempArr[0], all_content)#一定規律下對字串拼接得到第二層地址, 得到真實m3u8下載地址,
key_url = "%s/%s/hls/key.key" % (tempArr[0], midStr)#分析規律對字串拼接得到key的地址
key_html = requests.head(key_url)#訪問key的地址得到的文字
status = key_html.status_code#是否成功訪問到key的地址
key = ""
if status == 200:
all_content=requests.get(realAdr).text#請求第二層m3u8檔案地址得到內容
if "#EXT-X-KEY" in all_content:
key = requests.get(key_url).content#如果其中有"#EXT-X-KEY"這個欄位說明視訊被加密
self.fileName = bsObj.find(class_="video-title w100").contents[0].contents[0]#分析網頁得到視訊的名稱
self.fileName=re.sub(r'[\s,!]','',self.fileName)#因為如果檔名中有逗號感嘆號或者空格會導致合併時出現命令不正確錯誤,所以通過正則表示式直接去掉名稱中這些字元
iv = b'abcdabcdabcdabcd'#AES解密時候湊位數的iv
if len(key):#如果key有值說明被加密
cryptor = AES.new(key, AES.MODE_CBC, iv)#通過AES對ts進行解密
urlList=self.get_url_from_m3u8(realAdr)
urlRoot=tempArr[0]
i=1
outputfile=open(os.path.join(self.final_path,'%s.ts'%self.fileName),'wb')#初始建立一個ts檔案,之後每次迴圈將ts片段的檔案流寫入此檔案中從而不需要在去合併ts檔案
ip_list=self.get_ip_list(self.agency_url,self.headers)#通過網站爬取到免費的代理ip集合
for url in urlList:
try:
proxies=self.get_random_ip(ip_list)#從ip集合中隨機拿到一個作為此次訪問的代理
resp = requests.get("%s/%s/hls/%s" % (urlRoot, midStr, url.uri), headers=crawler.headers,proxies=proxies)#拼接地址去爬取資料,通過模擬header和使用代理解決封IP
if len(key):
tempText=cryptor.decrypt(resp.content)#解密爬取到的內容
progess=i/len(urlList)#記錄當前的爬取進度
outputfile.write(tempText)#將爬取到ts片段的檔案流寫入剛開始建立的ts檔案中
sys.stdout.write('\r正在下載:%s,進度:%s %%'%(self.fileName,progess))#通過百分比顯示下載進度
sys.stdout.flush()#通過此方法將上一行程式碼重新整理,控制檯只保留一行
else:
outputfile.write(resp.content)
except Exception as e:
print("\n出現錯誤:%s",e.args)
continue#出現錯誤跳出當前迴圈,繼續下次迴圈
i+=1
outputfile.close()
print("下載完成!總共耗時%d s"%(time.time()-start_time))
self.del_tempfile()#刪除臨時檔案
def del_tempfile(self):
file_list=os.listdir(self.down_path)
for i in file_list:
tempPath=os.path.join(self.down_path,i)
os.remove(tempPath)
os.rmdir(self.down_path)
print('臨時檔案刪除完成')
if __name__=='__main__':
url=input("輸入地址:\n")
crawler=VideoCrawler(url)
crawler.run()
quitClick=input("請按Enter鍵確認退出!")
碰到的問題:
一、一開始以為電腦中Python環境中有模組就OK了,最後發現在Pycharm中自己虛擬的環境中還需要新增對應模組,
二、No module named Crypto.Cipher ,網上看了很多最後通過新增pycryptodome模組解決,電腦環境Win10
三、檔名不能有感嘆號,逗號或者空格等這些特殊字元,不然執行合併命令的時候會提示命令不正確
四、在下載中將ts檔案流寫入檔案時會出現這種錯誤('Data must be padded to 16 byte boundary in CBC mode',) Data must be padded,我們直接continue跳出當前迴圈繼續下次下載。
五、有時出現 “Protocol Error, Connection abort, os.error”,應該是爬取操作太頻繁ip被封,針對此問題我們使用免費代理。
ok我們可以生成exe去開心的下載自己想要的視訊了,happy!!!
想要獲取原始碼的 網址就算了哈 加群:1136192749