selenium+aiohttp+aiofiles+ffmpeg → m3u8

前言

        在获取视频时,有的网站是将完整的视频链接路径放在了 中,但是如果直接将如 mp4 文件放在网页中进行加载,如果视频由于时长、清晰度等原因致使过大,可能会导致视频加载速度很慢,所以现在大部分网站采用流媒体网络传输协议(HLS),将一个视频切成了很多个小段,这样只需要加载 m3u8 文件,根据 m3u8 里的索引进行播放,简而言之如果你拉动进度条到一个时间点,就会加载这个时间点前后的视频片段,速度就会快很多,不过对于视频的爬取也会复杂不少。

HLS 协议

        HLS 即 HTTP Live Streaming 是一个由苹果公司提出的基于 HTTP 的流媒体网络传输协议,他把整个流分成一个个小的基于 HTTP 的文件来下载,每次只下载一些,在开始一个流媒体会话时,客户端会下载一个包含元数据的 extended M3U playlist 文件,用于寻找可用的媒体流,m3u8 即编码格式为 UTF-8 的 M3U 文件。

        HLS 协议规定视频的封装格式是 ts,所以 m3u8 文件中,是一个 ts 的列表,也就是告诉浏览器可以播放这些 ts 文件,所以将所有的 ts 文件合并起来就可以得到一个完整的 mp4 视频文件。

ffmpeg 的安装

        ffmpeg 是一套完备的多媒体支持库,它几乎实现了所有当下常见的数据封装格式、多媒体传输协议以及音视频编、解码器,这里通过他将所有的 ts 文件合并成 mp4 视频文件。

ffmpeg Github 官方下载地址:Releases · BtbN/FFmpeg-Builds · GitHub

我下载的是:ffmpeg-master-latest-win64-gpl.zip,下载之后解压即可,bin 目录以下会生成以下三个文件:

selenium+aiohttp+aiofiles+ffmpeg → m3u8_第1张图片

  • ffmpeg.exe:音视频转码、转换器
  • ffplay.exe:简单的音视频播放器
  • ffprobe.exe:简单的多媒体码流分析器 

以下及安装成功:

selenium+aiohttp+aiofiles+ffmpeg → m3u8_第2张图片

  • libavcodec:包含音视频编码器和解码器
  • libavutil:包含多媒体应用常用的简化编程的工具,如随机数生成器、数据结构、数学函数等功能
  • libavformat:包含多种多媒体容器格式的封装、解封装工具
  • libavfilter:包含多媒体处理常用的滤镜功能
  • libavdevice:用于音视频数据采集和渲染等功能的设备相关
  • libswscale:用于图像缩放和色彩空间和像素格式转换功能
  • libswresample:用于音频重采样和格式转换等功能

获取视频

页面分析

视频网页链接:http://www.meijutt.org/fzls/yueyudiyiji/0-1.html

selenium+aiohttp+aiofiles+ffmpeg → m3u8_第3张图片

        右键点击查看网页源代码,Ctrl + F 检索关键词

selenium+aiohttp+aiofiles+ffmpeg → m3u8_第4张图片

点击打开 m3u8 文件:

selenium+aiohttp+aiofiles+ffmpeg → m3u8_第5张图片

  • #EXTM3U:每个 M3U 文件第一行必须是这个 tag,提供标示作用
  • #EXT-X-VERSION:用以标示协议版本,这里是3, 那么这里用的就是HLS协议第三个版本,此标签只能有0或1个,不写代表使用版本1
  • #EXT-X-MEDIA-SEQUENCE:切片的开始序号。每一个切片都有唯一的序号,相邻之间序号 +1。这个编号会继续增长,保证流的连续性,一个 media URI 并不是必须要包含的,如果没有,默认为 0
  • #EXT-X-ALLOW-CACHE:是否允许做 cache,这个可以在 PlayList 文件中任意地方出现,并且最多出现一次,作用效果是所有的媒体段
  • #EXT-X-TARGETDURATION:指定最大的媒体段(切片)时间长(秒)。所以 #EXTINF 中指定的时间长度必须小于或是等于这个最大,有些 Apple 设备这个参数不正确会无法播放
  • #EXTINF: 代表这个 ts 文件的切片时长,一般不超过 10s
  • #EXT-X-ENDLIST:表示 PlayList 的末尾了,文件结束符号,表示不再向播放列表文件添加媒体文件,它可以在 PlayList 中任意位置出现,但是只能出现一个
  • #EXT-X-PLAYLIST-TYPE:类型,vod 表示点播,live 表示直播
  • #EXT-X-KEY:加密

更多 m3u8 参数可参考:m3u8文件参数详解

视频直播与视频点播:视频直播和点播的主要区别

        Ctrl + F 检索 m3u8 可以看到 m3u8 文件的路径,并发现其放在 iframe 中,即页面层级嵌套了两个子页面,我们需要的 m3u8 文件路径就放在第二个子页面中,我们需要获取 m3u8 文件的路径然后下载其中的 ts 文件并进行合并,但直接获取网页代码是抓取不到这些内容的,因为直接通过 requests 请求拿到的页面源代码是没经过渲染的,所以拿不到 iframe 里面的内容,这里采用 selenium 提取相关信息:

selenium+aiohttp+aiofiles+ffmpeg → m3u8_第6张图片

iframe 相关内容可参考:https://blog.csdn.net/Yy_Rose/article/details/121682665

所以基本流程就是:

  1. 获取 iframe 中源码内容
  2. 获取 m3u8 地址
  3. 获取 m3u8 内容
  4. 下载 ts 文件
  5. 合并视频文件

获取 iframe 中源码内容 

import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver import ChromeOptions
import time
# 浏览器配置
option = ChromeOptions()
option.add_argument("--headless")  # 指定无头模式
driver = webdriver.Chrome(options=option)
driver.get(url)
# driver.implicitly_wait(10)
# 等待,避免网页数据没加载完全以至于获取不到
time.sleep(5)
iframe = driver.find_elements(By.TAG_NAME, "iframe")[1]
driver.switch_to.frame(iframe)
time.sleep(5)
# 获取 iframe 中内容
ifm_html = driver.page_source

获取 m3u8 地址

检索 "url":,只匹配到一个数据,即可通过正则对此进行匹配:

re_url = re.compile(r'"url": "(.*?)"', re.S)
# 获取 m3u8 的地址链接
m3u8_url = re_url.findall(ifm_html)[0]
print(f"m3u8 的地址为:{m3u8_url}")

获取 m3u8 内容

通过获取到的 m3u8 地址,下载保存其中内容:

# 将 m3u8 写入文件
m3u8_file = requests.get(m3u8_url, headers=headers)
with open('越狱.m3u8', mode='wb') as f:
    f.write(m3u8_file.content)
print("m3u8 文件下载完毕")

selenium+aiohttp+aiofiles+ffmpeg → m3u8_第7张图片

下载 ts 文件

        m3u8 文件中前面不带 # 的为 ts 文件路径,总共有 525 个 ts 文件,由于 ts 文件过多,同步一条条下载会导致速度过慢,这里采用异步下载:

async def download_ts(url, name, session):
    async with session.get(url) as resp:
        # 创建一个新文件夹 movie_ts 并设置为 excluded,下载过程将不占用 pycharm 缓存
        async with aiofiles.open(f'movie_ts/{name}.ts', mode='wb') as file:
            # 将下载到的内容写入到文件中
            await file.write(await resp.content.read())
    print(f"{name} 下载完毕")


async def download_movie():
    tasks = []
    # 从 m3u8 文件中获取每个 ts 文件的下载地址
    async with aiohttp.ClientSession() as session:
        async with aiofiles.open('越狱.m3u8', mode='r', encoding='utf-8') as f:
            async for line in f:
                # 去掉前面带 # 的非 ts 数据
                if line.startswith('#'):
                    continue
                # 去掉空格和换行
                ts_url = line.strip()
                # https://pic.url.cn/qqgameedu/0/d54180feb8f34f6f3eba5cebe6b66107_0/0
                # 每个 ts 文件以 d54180feb8f34f6f3eba5cebe6b66107_ 命名
                domain = "https://pic.url.cn/qqgameedu/0/"
                name = ts_url.strip(domain)
                # 创建异步任务
                task = asyncio.create_task(download_ts(ts_url, name, session))
                tasks.append(task)
            # 等待任务结束
            await asyncio.wait(tasks)

selenium+aiohttp+aiofiles+ffmpeg → m3u8_第8张图片

合并视频文件

ffmpeg 可以将一个 mp4 格式的视频分解成 ts 文件,并生成一个 m3u8 文件:

ffmpeg -y -i movie.mp4 -vcodec copy -acodec copy -vbsf h264_mp4toannexb movie.ts
ffmpeg -i movie.ts -c copy -map 0 -f segment -segment_list movie.m3u8 -segment_time 5 test-%03d.ts
  • segment_time: 切片时间,上面设置的每个切片时间为 5 秒
  • test-:切片的前缀
  • -acodec:使用 codec 编、解码
  • -map file:stream:设置输入流映射
  • -vcodec codec:强制使用 codec 编、解码方式,如果用 copy 表示原始编解码数据必须被拷贝
  • input_streams:记录输入流的信息,在 open_input_file 时初始化里面的部分数据
  • output_streams:记录输出流的信息,当 ffmpeg 选项为 -c copy 时,会在 init_output_stream_streamcopy 函数中将输入流的编解码信息拷贝给输出流

ffmpeg 可以分解 mp4 文件为一个个 ts 文件,自然也可以使用它将 ts 切片合并成一个完整的 mp4 文件:

将 ts 文件路径写入 txt 文档中,格式例如:

file G:/Python/m3u8/merge/1a0de66077eab0eea7aa9251a9deded4_.ts
with open("越狱.m3u8", mode='r', encoding='utf-8') as f:
    with open("file.txt", mode='w', encoding='utf-8') as file:
        for line in f:
            if line.startswith('#'):
                continue
            line_ts = line.strip()
            domain = "https://pic.url.cn/qqgameedu/0/"
            ts_name = line_ts.strip(domain)
            # 更改路径
            ts_path = f"file G:/Python/m3u8/merge/{ts_name}.ts"
            file.write(ts_path + "\n")
        file.close()
        print("保存完毕!")

selenium+aiohttp+aiofiles+ffmpeg → m3u8_第9张图片

# -safe 0: 防止 Operation not permitted
ffmpeg -y -f concat -safe 0 -i file.txt -strict -2 prison.mp4
ffmpeg -y -f concat -safe 0 -i file.txt -c copy prison.mp4
# 网上还有不少拼接方法 
  • -f:输出格式
  • -y:覆盖输出文件,即如果 prison.mp4 文件已经存在的话,直接覆盖掉
  • -i::输入文件的名字
  • concat:合并视频

报错解决:

[concat @ 000001935d022e80] Unsafe file name 'G:/XX/XX/XX/X.ts'
file.txt: Operation not permitted

解决办法:加上 -safe 0

[mp4 @ 0000022fe210adc0] Could not find tag for XXX in stream #0, codec not currently supported in container
Could not write header for output file #0 (incorrect codec parameters ?): Invalid argument
Error initializing output stream 0:0 --

解决办法:加上 -strict -2 或者 -acodec aac,表示 aac 音频编码

ts 文件被混淆为图片

        但我这里 XXX 处显示为 codec bmp,bmp 为位图(点阵图),证明视频被 ffmpeg 解析为了图片,ffmpeg 无法进行拼接,用以上解决方法可以拼接出 mp4 视频文件,但无法播放,所以可以看出 ts 文件后缀被批量修改为 bmp 了,这应该就是为什么将文件后缀改为 ts 可以播放视频,但是无法使用 ffmpeg 进行拼接的原因,文件路径用 Chrome 打开也显示为图片样式:

selenium+aiohttp+aiofiles+ffmpeg → m3u8_第10张图片

每个 0 即为 ts 片段,content-type:image/bmp,即证明观点,为 bmp(BitMap)格式图片文件: 

selenium+aiohttp+aiofiles+ffmpeg → m3u8_第11张图片

BMP、PNG、JPG 格式图片的区别:BMP GIF PNG JPG等图片格式的区别和适用情况 

  • BMP:文件体积大,较老的图片格式
  • PNG:文件体积小,较新的图片格式 

二进制文件通过 Notepad++ 的 Hex-Editer 插件可以看到,42 4d 表示为 BM,正常的 ts 文件是以红圈位置为开头的: 

selenium+aiohttp+aiofiles+ffmpeg → m3u8_第12张图片

所以将其之前的 delete 后保存即可,或者只删掉 42 4d,再用以下命令即可成功将 ts 合成为 mp4 视频:

ffmpeg -y -f concat -safe 0 -i file.txt -c copy prison.mp4

selenium+aiohttp+aiofiles+ffmpeg → m3u8_第13张图片

用了两个 ts 片段进行测试,没报错,合并成功,并且能够正常播放视频!!!

ffmpeg 使用可参考:https://blog.csdn.net/matthew0618band/article/details/9830681 

总结

        以上是对 m3u8 视频文件获取的实战归纳总结,以及 ts 被混淆为图片的解决办法,PNG 格式图片与上述 BMP 格式图片处理方法基本一致,如有见解欢迎评论区或私信指正交流~

参考资料:

https://blog.csdn.net/davidullua/article/details/120562737

https://blog.csdn.net/weixin_43841155/article/details/122229315

https://blog.csdn.net/qq_33697094/article/details/112718101

https://blog.csdn.net/yao_hou/article/details/117160767

https://blog.csdn.net/u011344582/article/details/122803304

你可能感兴趣的:(python爬虫,音视频,selenium,python)