python多线程爬取m3u8视频(包含AES解密)

python爬取m3u8视频(包含AES解密)

前情提要

部分代码摘录于某位大哥(写代码的时候收藏书签了的打算写博客的时候带上链接的,无奈手贱删除了chrome用户,所有的书签也没了,找到再补上),在此基础上添加了多线程,日志,以及防止重复下载文件的代码,弃用“copy \b”合并视频,发现新大陆“ffmpeg”
敲黑板一次:有时拿到的m3u8地址并不是真正的m3u8地址,它包含了真实的m3u8地址,如下图
在这里插入图片描述
敲黑板两次:m3u8中给出的ts文件有的是经过加密的,所以需要解密,如下图:
python多线程爬取m3u8视频(包含AES解密)_第1张图片
敲黑板三次对于最终下载下来的一堆ts文件,要合并,之前找了一堆合并软件发现都有各种各样的问题。其实系统自带的“copy \b”命令很好用,但是无奈这个命令对合并的文件命名要求有点麻烦。比如我有文件**000.ts,**001.ts,**002.ts,…,**999.ts,**1000.ts等,当文件合并到999的时候就停下了。当然自己下载文件的时候改一下文件的命名就可以了。但是自己后来发现另一个牛b的工具ffmepg。

贴代码

# -*- coding:utf-8 -*-
import os
import sys
import requests
import datetime
import threading
from Crypto.Cipher import AES
from binascii import b2a_hex, a2b_hex
import logging

reload(sys)
sys.setdefaultencoding('utf-8')

logfile = 'trans.log'
logger = logging.getLogger(__name__)
logger.setLevel(level=logging.DEBUG)

handler = logging.FileHandler(logfile, mode='a')
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter("%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s")
handler.setFormatter(formatter)
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(formatter)
logger.addHandler(handler)
logger.addHandler(console)


def download(url, num_thread=4):
    """
    :param url: m3u8文件url
    :param num_thread: 启动线程数
    :return:
    """
    download_path = os.getcwd() + "\\download"
    if not os.path.exists(download_path):
        os.mkdir(download_path)

    all_content = requests.get(url).text  # 获取第一层M3U8文件内容
    if "#EXTM3U" not in all_content:
        logger.error("非M3U8的链接")
        return

    if "EXT-X-STREAM-INF" in all_content:  # 第一层
        file_line = all_content.split("\n")
        for line in file_line:
            if '.m3u8' in line:
                url = url.rsplit("/", 1)[0] + "/" + line  # 拼出第二层m3u8的URL
                all_content = requests.get(url).text

    file_line = all_content.split("\n")

    unknow = True
    key = ""
    # 已下载列表
    local_list = os.listdir(download_path)
    # 所有要下载列表
    all_list = []
    # 待下载列表
    s_list = []
    for index, line in enumerate(file_line): 
        if "#EXT-X-KEY" in line:
            # 有的网站提供的ts格式的视频是经过AES加密的,需要解密
            method_pos = line.find("METHOD")
            comma_pos = line.find(",")
            method = line[method_pos:comma_pos].split('=')[1]
            logger.info("Decode Method:%s" % method)

            uri_pos = line.find("URI")
            quotation_mark_pos = line.rfind('"')
            key_path = line[uri_pos:quotation_mark_pos].split('"')[1]

            key_url = url.rsplit("/", 1)[0] + "/" + key_path  # 拼出key解密密钥URL
            res = requests.get(key_url)
            key = res.content
            # key = "9826c9209bdddcbe"
            logger.info("key:%s" % key)

        if "EXTINF" in line:  
            unknow = False
            # 找ts文件名
            file_name = file_line[index + 1]  
            all_list.append(file_name )

    if unknow:
        logger.error("未找到对应的下载链接")
    else:
        # 启动多线程下载文件
        s_list = list(set(all_list) - set(local_list))
        file_size = len(s_list)
        part = file_size // num_thread  # 如果不能整除,最后一块应该多几个字节
        thread_list = []

        for i in range(num_thread):
            start = part * i
            if i == num_thread - 1:  # 最后一块
                end = file_size
            else:
                end = start + part
            if end > start:
                t = threading.Thread(
                    target=multi_download,
                    kwargs={'start': start, 'end': end, 'url': url.rsplit("/", 1)[0] + "/", 'file_list': s_list, 'key': key})
                t.setDaemon(True)
                t.start()
                thread_list.append(t)
                logger.info("启动线程%s" % t.name)

    # 等待所有线程下载完成

    for t in thread_list:
        t.join()
        logger.info("结束线程%s" % t.name)
    logger.info('所有线程结束')


def multi_download(start, end, url, file_list, key):
    """

    :param start: 下载文件列表开始索引
    :param end: 下载文件列表结束索引
    :param url: 下载ts文件的url
    :param file_list: 下载文件列表
    :param key: AES解密用的key
    :return:
    """
    download_path = os.getcwd() + "\\download"
    for i in file_list[start:end]:
        global count
        down_url = url + i

        try:
            logger.debug("正在下载:%s" % i)
            res = requests.get(down_url)
            if key:  # AES 解密
                cryptor = AES.new(key, AES.MODE_CBC, key)
                with open(os.path.join(download_path, i), 'ab') as f:
                    f.write(cryptor.decrypt(res.content))
            else:
                with open(os.path.join(download_path, i), 'ab') as f:
                    f.write(res.content)
                    f.flush()

        except:
            logger.warning("下载失败:%s" % i)
        count = count + 1
        logger.info("下载进度:%.2f%s-----%s/%s" % (100*float(count) / len(file_list), "%", count, len(file_list)))


def merge_file(path):
    os.chdir(path)
    # 直接调用系统copy命令
    cmd = "copy /b *.ts new.mp4"
    os.system(cmd)


if __name__ == '__main__':
    count = 0
    m3u8_url = "https://****/hls/index.m3u8"
    download(m3u8_url)
    # download_path = os.getcwd() + "\\download"
    # merge_file(download_path)

当有文件下载失败时,直接重新运行即可下载之前下载失败的文件。

ffmpeg

下载ffmpeg后,把m3u8文件下载到本地,执行命令:ffmpeg -i m3u8文件本地路径 -c copy new.mp4。
也可直接执行命令:ffmpeg -i m3u8文件url -c copy new.mp4 直接下载完整视频。但是如果遇到网络影响容易失败,卡住切无法多线程下载文件。但是ffmpeg有很多其他强大的功能,自行查找相关知识吧。

一行代码爬取视频

除了 ffmpeg 可以一行代码下载需要的视频,还有另一样神器:you-get,只需要执行命令:“you-get 视频url”。只支持部分视频网站(youtube,腾讯视频,b站等)。

遇到的问题

报出SNIMissingWarning和InsecurePlatformWarning警告
解决方法:pip install pyopenssl ndg-httpsclient pyasn1

你可能感兴趣的:(python,爬虫,多线程)