python 實現多線程下載m3u8格式視頻,使用FFmpeg合併

如果你需要下載的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信息

效果如圖:

爬蟲開始及文件寫入:

下載開始,下載與寫文件耗時:

說明:文件寫入的數字是一直在變化的,用以進度觀察。任務文件也是。

結束:

文件夾:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章