这篇会用到 python 的几个常用库,非常实用,建议收藏
最近刷短视频,老是看到《危机边缘》这部美剧,感觉很好看。但是找到的一些网站看的时候,不知道什么原因,经常卡顿,体验贼差。我就在想,要不要下载下来看?
一开始我简单以为就是个 mp4 文件,想想写个油猴脚本估计就能搞定 —— 找到资源链接,然后用我常用的下载工具(Neat Downloader Manager)开32个线程并行下载,这不是 so easy??
咋回事?和我的预期差了十万八千里。 没有 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
我试了下,确实还不错,下载速度也还可以。
不过,我还是不满足,这个 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,内容大概是这样
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