为了稳定看美剧,我居然倒腾了这个

311 阅读5分钟

这篇会用到 python 的几个常用库,非常实用,建议收藏

最近刷短视频,老是看到《危机边缘》这部美剧,感觉很好看。但是找到的一些网站看的时候,不知道什么原因,经常卡顿,体验贼差。我就在想,要不要下载下来看?

一开始我简单以为就是个 mp4 文件,想想写个油猴脚本估计就能搞定 —— 找到资源链接,然后用我常用的下载工具(Neat Downloader Manager)开32个线程并行下载,这不是 so easy??

image.png

image.png

咋回事?和我的预期差了十万八千里。 没有 mp4 的链接,而且 m3u8 是什么格式?

m3u8准确来说是一种索引文件,使用m3u8文件实际上是通过它来解析对应的放在服务器上的视频网络地址,从而实现在线播放。使用m3u8格式文件主要因为可以实现多码率视频的适配,视频网站可以根据用户的网络带宽情况,自动为客户端匹配一个合适的码率文件进行播放,从而保证视频的流畅度。

其实我一点也不关心 m3u8 是什么,我就想知道如何下载= =

ffmpeg

我看到一种方法,可以用 ffmpeg 来实现下载成 mp4 格式。

先安装ffmpeg,因为我这里是 mac 电脑,所以下面的都是以 mac 为例子

brew install ffmpeg
ffmpeg -i https://b1.szjal.cn/20210717/L80VKwIz/index.m3u8 /Users/xxx/Desktop/demo.mp4

我试了下,确实还不错,下载速度也还可以。

image.png

不过,我还是不满足,这个 m3u8 的链接还要我手动去复制,再调用命令,着实有点不方便。

嗯,那我们就再想想别的办法。

python 爬虫

自然而然,我就想到从 python 爬虫的角度来搞定这个事情。

大体思路,从网页中提取 m3u8 的链接,下载 m3u8 文件,读取里面的 .ts 文件列表,然后再把所有 .ts 文件下载下来,最后按照顺序把 .ts 文件的内容写到 mp4 文件中就可以了。

传参网页地址

首先,为了方便命令行传参数给 python 脚本,我用了 argparse 这个库

# fetch_m3u8.py
import argparse

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='download mp4 from html')
    parser.add_argument('--url', dest="url",
                        help='url of the html page', required=True)
    parser.add_argument('--name', dest="name", help="video name", required=True)
    args = parser.parse_args()
    download_mp4(args.url, args.name)
./fetch_m3u8.py --url https://www.eeuu88.com/xy/4075811.html --name 危机边缘第二季

遭遇 403,防爬虫

import urllib.request

def get_m3u8(url):
    r = urllib.request.urlopen(url)
    data = bytes.decode(r.read(), "utf-8")

我想要通过网页的url,获取 html 的时候,遇到了问题 —— 服务器 403!

好吧,看来还是得伪装一下。

def get_m3u8(url):
    opener = urllib.request.build_opener()
    opener.addheaders = [
        ('User-agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.61 Safari/537.36')]
    urllib.request.install_opener(opener)
    r = urllib.request.urlopen(url)
    data = bytes.decode(r.read(), "utf-8")

我通过伪造 user-agent 简单骗过了服务器,看来不咋滴嘛~

获取到 html 后,就要从里面获取 m3u8 的链接。运气不错,通过观察,我发现有一个变量记录了 m3u8 的链接,以及下一集的网页 url 地址,直接免去了不少麻烦。

BeautifulSoup 解析 html

这里我借助 BeautifulSoup 来解析 html,有点像 jquery 查找 dom 元素。

BeautifulSoup 不是标准库,所以要先安装 我机器上是 python@3.9 的环境,所以我直接 pip install bs4 完成了安装

import bs4 from BeautifulSoup

def get_target_script(html):
    soup = BeautifulSoup(html, "lxml")
    scripts = soup.find_all("script")
    if scripts is None:
        sys.exit("no scripts")
    for script in scripts:
        # ...code 具体代码省略

def get_m3u8_url(html)
    script = get_target_script(html)
    if script is None:
        sys.exit("no script")
    m3u8 = re.search(r'(http.*?\.m3u8)', script.string).group(1)
    next_url = re.search(r'\"next\":\"([^\"]*?)\"', script.string).group(1)
    return m3u8, next_url

为了 安全,上面的代码做了一下省略,具体是什么变量存储了 m3u8 的地址需要大家自己去观察。当然,网站的 url, 我在前面的代码中已经给出了。

现在,我拿到了 m3u8 的地址以及下一集的网页地址了。可以开始着手对付 m3u8 了

下载 m3u8 以及内部的 .ts 文件

包含 .ts 文件列表的 m3u8,内容大概是这样

image.png

import urllib3

def get_ts_list(urlParser, data):
    ts_files = []
    if data.find('.ts'):
        for l in r.data.splitlines():
            if bytes.decode(l, 'utf-8').endswith(".ts"):
                ts_files.append(urlParser.scheme + '://' +
                                urlParser.netloc + bytes.decode(l, 'utf-8'))
    return ts_files

def download_m3u8_ts(url):
    urlParser = urllib.parse.urlparse(url)
    http = urllib3.PoolManager()
    data = bytes.decode(r.data, "utf-8")
    if data.find(".m3u8") != -1:
        target_m3u8 = None
        for l in r.data.splitlines():
            if bytes.decode(l).endswith(".m3u8"):
                target_m3u8 = bytes.decode(l)
        if target_m3u8 is None:
            sys.exit('No m3u8 file found')
        return download_m3u8_ts(result.scheme + '://' + result.netloc + target_m3u8)
    
    ts_files = get_ts_list(urlParser, data)
     

接下来,就是下载 .ts 文件了。这里我用的还是 ullib3.PoolManager

import urllib3
import os

def dowload_ts(files, parent_dir):
    http = urllib3.PoolManager()
    for file in files:
        # N4IGN1756233.ts
        filename = os.path.basename(file)
            # 避免重复下载
            if not os.path.exists(os.path.join(parent_dir, filename)):
                f = http.request('GET', file, preload_content=False)
                r = f.data
                with open(os.path.join(parent_dir, filename), 'wb') as out:
                    out.write(r)

合并 ts 文件生成 mp4 文件

最后这一步就简单了。

# files 是之前下载好的 ts 文件的本地路径的列表
# mp4_path: /xxxx/xxx/demo.mp4
def merge_ts_to_mp4(files, mp4_path):
    with open(mp4path, 'wb+') as out:
        for file in files:
            with open(file, 'rb') as f:
                out.write(f.read())

做到这一步,基本就可以了。不过还是有不足,现在的下载是一个一个接着下,我想要并发下载,提高文件下载速度。

优化:多线程下载文件

这里我借助的是 threading 提供的功能。

from thread import Thread
from math import floor
import urllib3

def thread_download(ts_files, parent_dir):
    threadCount = 8
    http = urllib3.PoolManager(num_pools=threadCount+1)
    if len(ts_files) <= threadCount:
        download_ts(http, files, parent_dir)
    else:
        cout = floor(len(ts_files)/threadCount)
        # 为了把文件下载任务分配到多个线程上,所以先分组
        new_files = [items[i:i+count] for i in range(0, len(ts_files), count)]
        threads = []
        for files in new_files:
            t = Thread(target=download_ts, args=(http, files, parent_dir))
            threads.append(t)
        for t in thread:
            t.start()
        for t in thread:
            # 等待所有线程任务都结束
            t.join()
        

我计划使用8个线程同时下载文件,实际效果还不错。

本来还想要不要再写一个界面,不过可能影响到我周末看剧的时间,就有时间再弄了 = =

这个完整脚本,我放在这里了,有需要的同学自己下载后修改即可:fetch_m3u8