如果你需要下載的m3u8文件被加密
請移步:https://blog.csdn.net/s_kangkang_A/article/details/103163073
電影之類的長視頻好像都用m3u8格式了,這就導致了多線程下載視頻的意義不是很大,都是短視頻,線不線程就沒什麼意義了嘛。
我們知道,m3u8的鏈接會下載一個文檔,相當長,半小時的視頻,應該有接近千行ts鏈接。
這些ts鏈接下載成ts文件,就是碎片化的視頻,加以合併,就成了需要的視頻。
那,即便網速很快,下幾千行視頻,效率也就低了,更何況還要合併。我就琢磨了一下午,怎麼樣才能多線程下載m3u8格式的視頻呢?
先上代碼,再說重難點:
import datetime
import os
import re
import threading
import requests
from queue import Queue
# 預下載,獲取m3u8文件,讀出ts鏈接,並寫入文檔
def down():
# m3u8鏈接
url = 'https://ali-video.acfun.cn/mediacloud/acfun/acfun_video/segment/3zf_GAW6nFMuDXrTLL89OZYOZ4mwxGoASH6UcZbsj1_6eAxUxtp3xm8wFmGMNOnZ.m3u8?auth_key=1573739375-474267152-0-a5aa2b6df4cb4168381bf8b04d88ddb1'
# 當ts文件鏈接不完整時,需拼湊
# 大部分網站可使用該方法拼接,部分特殊網站需單獨拼接
base_url = re.split(r"[a-zA-Z0-9-_\.]+\.m3u8", url)[0]
# print(base_url)
resp = requests.get(url)
m3u8_text = resp.text
# print(m3u8_text)
# 按行拆分m3u8文檔
ts_queue = Queue(10000)
lines = m3u8_text.split('\n')
# 找到文檔中含有ts字段的行
concatfile = 'cache/' + "s" + '.txt'
for line in lines:
if '.ts' in line:
if 'http' in line:
# print("ts>>", line)
ts_queue.put(line)
else:
line = base_url + line
ts_queue.put(line)
# print('ts>>',line)
filename = re.search('([a-zA-Z0-9-]+.ts)', line).group(1).strip()
# 一定要先寫文件,因爲線程的下載是無序的,文件無法按照
# 123456。。。去順序排序,而文件中的命名也無法保證是按順序的
# 這會導致下載的ts文件無序,合併時,就會順序錯誤,導致視頻有問題。
open(concatfile, 'a+').write("file %s\n" % filename)
return ts_queue,concatfile
# 線程模式,執行線程下載
def run(ts_queue):
tt_name = threading.current_thread().getName()
while not ts_queue.empty():
url = ts_queue.get()
r = requests.get(url, stream=True)
filename = re.search('([a-zA-Z0-9-]+.ts)', url).group(1).strip()
with open('cache/' + filename, 'wb') as fp:
for chunk in r.iter_content(5242):
if chunk:
fp.write(chunk)
print(tt_name + " " + filename + ' 下載成功')
# 視頻合併方法,使用ffmpeg
def merge(concatfile, name):
try:
path = 'cache/' + name + '.mp4'
command = 'ffmpeg -y -f concat -i %s -crf 18 -ar 48000 -vcodec libx264 -c:a aac -r 25 -g 25 -keyint_min 25 -strict -2 %s' % (concatfile, path)
os.system(command)
print('視頻合併完成')
except:
print('合併失敗')
if __name__ == '__main__':
name = input('請輸入視頻名稱:')
start = datetime.datetime.now().replace(microsecond=0)
s,concatfile = down()
# print(s,concatfile)
threads = []
for i in range(15):
t = threading.Thread(target=run, name='th-'+str(i), kwargs={'ts_queue': s})
threads.append(t)
for t in threads:
t.start()
for t in threads:
t.join()
end = datetime.datetime.now().replace(microsecond=0)
print('下載耗時:' + str(end - start))
merge(concatfile,name)
over = datetime.datetime.now().replace(microsecond=0)
print('合併耗時:' + str(over - end))
效果圖:
代碼開始:自己輸入視頻名稱(也可以去原網站爬名稱)
查看下載耗時,ffmpeg開始合併:
合併耗時:
7分多鐘,90個ts文件,接近40MB。兩秒下載完成。
更大的文件,開更多的線程。
然後我們畫畫重難點:
第一:ts文件命名問題。
我們知道,每一個線程啓動,除了隊列不會重複,那麼代碼裏都會重新跑(線程裏的代碼),那麼,1.ts,2.ts....這種命名是不可能的了,文件會被覆蓋。命名我使用了ts鏈接中的部分鏈接。
第二:合併問題。
文件的合併是根據文檔內的順序,也就是,如果邊下載邊合併,那麼,線程的無序性導致下載無序,文件寫入也就無序化了,合併時,時間線會錯誤,合出來的視頻就無法看。因此,文件要提前寫好纔行,這和命名有很大的關聯,看代碼即知。
第三:有的m3u8是特殊處理的,代碼具有一定的侷限性。
寫的時候挺難的,腦子都亂了,就這些吧,記錄一下。
對了,貼一下下載的圖:90個ts文件,一個mp4文件,一個文檔。
—————2019/11/18更新——————
更新點細節和後續處理。
今天找了個m3u8文件的電影鏈接,銀河補習班,用代碼測試,遇到了報錯,等會放到我的報錯指南里。
然後,解決了報錯,下載很快,一個1.72G的電影,一分鐘左右下載完成(線程數50)
但是,合併合一個小時???what f**k!!!
更新一下新的合併方法,順便把下載的兩千多個ts刪除,簡化文件夾:
import datetime
import os
import re
import threading
import requests
from queue import Queue
# 預下載,獲取m3u8文件,讀出ts鏈接,並寫入文檔
def down(headers):
url = 'https://www.mmicloud.com:65/ppvod/PkOhYba8'
# 當ts文件鏈接不完整時,需拼湊
# 大部分網站可使用該方法拼接,部分特殊網站需單獨拼接
# base_url = re.split(r"[a-zA-Z0-9-_\.]+\.m3u8", url)[0]
base_url = 'https://www.mmicloud.com:65'
print(base_url)
resp = requests.get(url,headers=headers)
m3u8_text = resp.text
print(m3u8_text)
# 按行拆分m3u8文檔
ts_queue = Queue(10000)
lines = m3u8_text.split('\n')
# 找到文檔中含有ts字段的行
concatfile = 'cache/' + "s" + '.txt'
for line in lines:
if '.ts' in line:
if 'http' in line:
# print("ts>>", line)
ts_queue.put(line)
else:
line = base_url + line
ts_queue.put(line)
# print('ts>>',line)
filename = re.search('([a-zA-Z0-9-_]+.ts)', line).group(1).strip()
# 一定要先寫文件,因爲線程的下載是無序的,文件無法按照
# 123456。。。去順序排序,而文件中的命名也無法保證是按順序的
# 這會導致下載的ts文件無序,合併時,就會順序錯誤,導致視頻有問題。
open(concatfile, 'a+').write("file %s\n" % filename)
return ts_queue,concatfile
# 線程模式,執行線程下載
def run(ts_queue, headers):
tt_name = threading.current_thread().getName()
while not ts_queue.empty():
url = ts_queue.get()
r = requests.get(url, stream=True, headers = headers)
filename = re.search('([a-zA-Z0-9-_]+.ts)', url).group(1).strip()
with open('cache/' + filename, 'wb') as fp:
for chunk in r.iter_content(5242):
if chunk:
fp.write(chunk)
print(tt_name + " " + filename + ' 下載成功')
# 視頻合併方法,使用ffmpeg
def merge(concatfile, name):
try:
path = 'cache/' + name + '.mp4'
# command = 'ffmpeg -y -f concat -i %s -crf 18 -ar 48000 -vcodec libx264 -c:a aac -r 25 -g 25 -keyint_min 25 -strict -2 %s' % (concatfile, path)
command = 'ffmpeg -y -f concat -i %s -bsf:a aac_adtstoasc -c copy %s' % (concatfile, path)
os.system(command)
print('視頻合併完成')
except:
print('合併失敗')
def remove():
dir = 'cache/'
for line in open('cache/s.txt'):
line = re.search('file (.*?ts)',line).group(1).strip()
# print(line)
os.remove(dir + line)
print("刪除成功")
if __name__ == '__main__':
name = input('請輸入視頻名稱:')
headers = {
'Accept': '*/*',
'Accept-Encoding': 'gzip,deflate,br',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Connection': 'keep-alive',
'Host': 'www.mmicloud.com:65',
'Origin': 'https://jx.123ku.com',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36 Maxthon/5.1.3.2000'
}
start = datetime.datetime.now().replace(microsecond=0)
s,concatfile = down(headers)
# 獲取隊列元素數量
num = s.qsize()
# 根據數量來開線程數,每五個元素一個線程
if num > 5:
t_num = num // 5
else:
t_num = 1
if t_num > 50:
t_num = 50
# print(s,concatfile)
threads = []
for i in range(t_num):
t = threading.Thread(target=run, name='th-'+str(i), kwargs={'ts_queue': s,'headers': headers})
t.setDaemon(True)
threads.append(t)
for t in threads:
t.start()
for t in threads:
t.join()
end = datetime.datetime.now().replace(microsecond=0)
print('下載耗時:' + str(end - start))
merge(concatfile,name)
over = datetime.datetime.now().replace(microsecond=0)
print('合併耗時:' + str(over - end))
remove()
合併改了命令,效率賊快,下載加合併接近4分鐘:
代碼開始:
下載開始到結束:
合併開始及結束:
文件夾:所有ts文件已被刪除:
txt:部分截圖:視頻詳情
另外,文中的鏈接具有時效性,一會就會失效。
————————19日更新——————
在更新一點小東西,可以直接用這個新版,前面不用看
import datetime
import os
import re
import threading
import time
import requests
from queue import Queue
# 預下載,獲取m3u8文件,讀出ts鏈接,並寫入文檔
def down(headers, url, base_url):
# 當ts文件鏈接不完整時,需拼湊
resp = requests.get(url, headers=headers)
m3u8_text = resp.text
# print(m3u8_text)
# 按行拆分m3u8文檔
ts_queue = Queue(10000)
lines = m3u8_text.split('\n')
s = len(lines)
# 找到文檔中含有ts字段的行
concatfile = 'cache/' + "s" + '.txt'
for i,line in enumerate(lines):
if '.ts' in line:
if 'http' in line:
# print("ts>>", line)
ts_queue.put(line)
else:
line = base_url + line
ts_queue.put(line)
# print('ts>>',line)
filename = re.search('([a-zA-Z0-9-_]+.ts)', line).group(1).strip()
# 一定要先寫文件,因爲線程的下載是無序的,文件無法按照
# 123456。。。去順序排序,而文件中的命名也無法保證是按順序的
# 這會導致下載的ts文件無序,合併時,就會順序錯誤,導致視頻有問題。
open(concatfile, 'a+').write("file %s\n" % filename)
print("\r", '文件寫入中', i, "/", s, end="", flush=True)
return ts_queue, concatfile
# 線程模式,執行線程下載
def run(ts_queue, headers):
while not ts_queue.empty():
url = ts_queue.get()
filename = re.search('([a-zA-Z0-9-_]+.ts)', url).group(1).strip()
try:
requests.packages.urllib3.disable_warnings()
r = requests.get(url, stream=True, headers=headers, verify=False)
with open('cache/' + filename, 'wb') as fp:
for chunk in r.iter_content(5242):
if chunk:
fp.write(chunk)
print("\r", '任務文件 ', filename, ' 下載成功', end="", flush=True)
except:
print( '任務文件 ', filename, ' 下載失敗')
ts_queue.put(url)
# 視頻合併方法,使用ffmpeg
def merge(concatfile, name):
try:
path = 'cache/' + name + '.mp4'
# command = 'ffmpeg -y -f concat -i %s -crf 18 -ar 48000 -vcodec libx264 -c:a aac -r 25 -g 25 -keyint_min 25 -strict -2 %s' % (concatfile, path)
command = 'ffmpeg -y -f concat -i %s -bsf:a aac_adtstoasc -c copy %s' % (concatfile, path)
os.system(command)
print('視頻合併完成')
except:
print('合併失敗')
def remove():
dir = 'cache/'
for line in open('cache/s.txt'):
line = re.search('file (.*?ts)', line).group(1).strip()
# print(line)
os.remove(dir + line)
print("ts文件全部刪除")
try:
os.remove('cache/s.txt')
print('文件刪除成功')
except:
print('文件刪除失敗')
if __name__ == '__main__':
name = input('請輸入視頻名稱:')
url = input('請輸入視頻鏈接:').strip()
# 測試用鏈接:https://yiyi.55zuiday.com/ppvod/70B5A6E3A150A99882E28EC793CAF519.m3u8
# 鏈接電影:地球最後的夜晚
base_url = 'https://yiyi.55zuiday.com/'
headers = {
'referer': 'https://yiyi.55zuiday.com/share/wVuAcJFy1tMy4t0x',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36'
}
start = datetime.datetime.now().replace(microsecond=0)
print("文件開始寫入")
s, concatfile = down(headers, url, base_url)
print('\n')
print("文件寫入結束")
# 獲取隊列元素數量
num = s.qsize()
# 根據數量來開線程數,每五個元素一個線程
# 最大開到50個
print("下載任務開始")
if num > 5:
t_num = num // 5
else:
t_num = 1
if t_num > 50:
t_num = 50
# print(s,concatfile)
threads = []
for i in range(t_num):
t = threading.Thread(target=run, name='th-' + str(i), kwargs={'ts_queue': s, 'headers': headers})
t.setDaemon(True)
threads.append(t)
for t in threads:
time.sleep(0.4)
t.start()
for t in threads:
t.join()
print('\n')
print("下載任務結束")
end = datetime.datetime.now().replace(microsecond=0)
print('寫文件及下載耗時:' + str(end - start))
merge(concatfile, name)
remove()
over = datetime.datetime.now().replace(microsecond=0)
print('合併及刪除文件耗時:' + str(over - end))
print("所有任務結束")
print('任務總時長:', over - start)
更新爲覆蓋打印,避免打印幾千行,太麻煩,寫文件和下載都有進度了
下載錯誤還可以看錯誤打印信息,正確直接下載覆蓋
清晰明瞭看見代碼運行進度。
然後就算根據需要改三個東西,就可以使用代碼
1,電影m3u8鏈接
2,m3u8文件中如ts鏈接無前面http部分,需自己更改base_url
3,更改headers信息
效果如圖:
爬蟲開始及文件寫入:
下載開始,下載與寫文件耗時:
說明:文件寫入的數字是一直在變化的,用以進度觀察。任務文件也是。
結束:
文件夾: