BiliBili视频下载-原理与实现Python+FFmpeg

脚本地址:

项目地址: Gazer

BiliGrab.py

提要

适用于: 登录状态下, 非大会员视频下载.

自动解析任意 B 站非大会员 / 付费视频的视频 & 音频请求链接并下载, 需要添加 Cookie 保证视频清晰度. 使用 FFmpeg 命令无损合并视频和音频.

使用方法

  1. 克隆或下载项目代码.
  2. 安装依赖: pip install requests, 或者克隆项目代码后 pip install -r requirements.txt
  • 脚本顶部: 指定常量 FOLDER_PATH, 保存视频和音频文件夹路径
  • 主函数处: 填写你的 cookie
  • 主函数处: 指定 url, 要下载的视频 URL
  • 主函数处: 指定 video_file_nameaudio_file_name, 要保存的视频和音频文件名

代码结构

  1. get_url 从 HTML 中的 window.__playinfo__ 提取解析视频的 2 个请求 URL
  2. download_video 接收 get_url 的返回链接, 下载视频/音频文件

抓包

登录状态下, 随机打开哔哩哔哩的一个视频, 打开 devtool, 播放一会视频, 让请求一一列出. 然后在 Network 面板的过滤器中, 输入 m4s, 过滤出与视频相关的请求. 点 Size 从大到小排列.

m4s 过滤可以同时过滤出音频和视频, Type 都是 application/octet-stream. 这证明了音频和视频数据都是以 m4s 格式存储的, 并且使用了相同的 MIME 类型.

请求为 ...100026... 的是视频请求网址, 请求为 ...30280... 的是音频请求网址.

点开请求可以看到, Status Code: 206 Partial Content, 表示服务器成功处理了部分 GET 请求, 而且说明视频内容是一部分一部分传输的.

复制视频/音频的请求 URL, 分别使用 requests get 一下, 能够成功下载完整视频和音频.

代码示例 (下载单个 m4s 片段):

python 复制代码
import requests

video_url = "Request_URL"  # 替换成你找到的请求链接

headers = {
    "Referer": "https://www.bilibili.com/",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
}

response = requests.get(video_url, headers=headers, stream=True)

if response.status_code == 200 or response.status_code == 206:
    with open("video_part.m4s", "wb") as f:
        for chunk in response.iter_content(chunk_size=8192):
            if chunk:
                f.write(chunk)
    print("下载成功!")
else:
    print("下载失败!")
    print(response.status_code)

自动解析视频和音频的请求 URL

现在这个脚本只能自己通过 devtool 来查看请求的 URL 然后复制到脚本再下载, 那么如何做到我们只需要输入 B 站视频的地址, 即可自动获取它的 2 个请求 (视频和音频) 的 URL 呢?

分析 B 站视频页面的 HTML 结构:

  • 找到包含视频和音频 URL 的 HTML 元素或 JavaScript 变量.
  • 通常, 视频和音频的 URL 会以某种形式嵌入在网页中, 例如:
    • <video> 标签的 src 属性中(但 B 站通常不是这样).
    • 在 JavaScript 变量中(例如一个名为 window.__playinfo__ 的变量, 这是 B 站常用的).
    • 在 JSON 数据中(例如一个包含视频信息的 JSON 对象).
  1. play_info 的提取:

    • 原先使用了 soup.select_one('head > script:nth-child(58)') 来定位包含 play_info<script> 标签. 这种方法依赖于 <script> 标签在 HTML 中的具体位置(第 58 个子元素). 如果 B 站的网页结构发生变化, 这个选择器可能会失效.
    • 更可靠的方法是使用正则表达式 window\.__playinfo__\s*=\s*({.*?"video":.*?"audio":.*?"session":.*?})\s* 来提取 window.__playinfo__ 变量的值.

正则表达式详解

regex 复制代码
window\.__playinfo__\s*=\s*({.*?"video":.*?"audio":.*?"session":.*?})\s*

这个正则表达式从 HTML 源代码中提取 B 站视频页面的 window.__playinfo__ 变量的值. window.__playinfo__ 是一个 JavaScript 变量, 它包含了视频和音频的播放信息(例如 URL、清晰度、编码等), 通常以 JSON 格式存储.

详细解释:

  1. window\.__playinfo__: 匹配字符串 "window.playinfo".

    • window: 匹配字面上的 "window".
    • \.: 匹配一个点号 ".". 由于点号在正则表达式中有特殊含义(匹配任意字符), 所以需要使用反斜杠 \ 进行转义.
    • __: 匹配两个下划线 "_".
    • playinfo: 匹配字面上的 "playinfo"
  2. \s*: 匹配零个或多个空白字符(空格、制表符、换行符等).

  3. =: 匹配等号 "=".

  4. \s*: 匹配零个或多个空白字符.

  5. ({.*?}): 这是整个正则表达式的核心, 用于捕获 window.__playinfo__ 变量的值(JSON 对象).

    • (): 定义一个捕获组, 用于提取匹配到的内容.
    • {}: 匹配大括号, 表示 JSON 对象的开始和结束.
    • .*?: 匹配任意字符(除了换行符), *? 表示非贪婪模式, 尽可能少地匹配字符.
      • .*?"video":: 匹配任意字符直到 "video": 出现.
      • .*?"audio":: 匹配任意字符直到 "audio": 出现.
      • .*?"session":: 匹配任意字符直到"session"出现.
      • .*?: 匹配任意字符直到 }.
  6. \s*: 匹配在结尾大括号后的零个或多个空白字符.

在 Python 中使用(示例):

python 复制代码
import re
import json

html = """
<script>
    window.__playinfo__ = {
        "data": {
            "dash": {
                "video": [
                    {"id": 30, "baseUrl": "video_url_360p"},
                    {"id": 80, "baseUrl": "video_url_1080p"}
                ],
                "audio": [
                    {"id": 30280, "baseUrl": "audio_url"}
                ],
                "session": "session_id"
            }
        }
    };
    // 其他 JavaScript 代码
</script>
"""

match = re.search(r'window\.__playinfo__\s*=\s*({.*?"video":.*?"audio":.*?"session":.*?})\s*', html)

if match:
    playinfo_json = match.group(1)  # 获取捕获组的内容(JSON 字符串)
    playinfo = json.loads(playinfo_json)  # 解析 JSON 字符串

    print(playinfo)

目前位置脚本没有加上 Cookie, 如果在自动解析之后仍然不带上 Cookie, 只能下载到 360P 的视频. 下面来确认一下:

1. 未登录状态:

在未登录状态下, B 站只能播放 360P 视频.

2. 参数缺失:

请求 URL 缺少某些参数. 通过比较手动抓取的 URL 和脚本抓取的 URL, 可以发现一些差异.

关键参数差异:

  • agrr=1: 高清 URL 包含 agrr=1, 而程序抓取的 URL 包含 agrr=0. 这可能表示 "Adaptive Group Representation Rate"(自适应组表示率), agrr=1 可能表示启用自适应码率, 允许选择更高清的视频流.
  • buvid: 高清 URL 包含 buvid=75265G3Y-C2S1-66P3-8396-2FVCDS983Y0925738infoc, 而程序抓取的 URL 包含 buvid=(为空). buvid 是 B 站用来识别用户的设备 ID.
  • mid: 高清 URL 包含 mid=73467246(你的用户 ID), 而程序抓取的 URL 包含 mid=0.
  • traceidsign: 高清 URL 包含 traceidsign, 而程序抓取的 URL 不包含. 这些参数可能是 B 站用来验证请求的合法性.

解决方案:

添加 Cookie:

  • 从浏览器中获取 B 站 Cookie(在登录状态下).

  • 将 Cookie 添加到 requests 请求头中.

    python 复制代码
    headers = {
        "Referer": "https://www.bilibili.com/",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
        "Cookie": "YOUR_COOKIE"  # 替换为你的 B 站 Cookie
    }

使用 FFmpeg 无损合并视频和音频

下载完音频后, 我们可以尝试将视频和音频合并起来. 这需要用到一些视频编辑工具, 例如 ffmpeg.

FFmpeg:

  • ffmpeg 是一个非常强大的命令行工具, 可以用来处理各种视频和音频文件.
  • 先安装 ffmpeg, 然后才能在命令行中使用它. 可以在 ffmpeg官网找到安装教程.

使用 FFmpeg 合并视频和音频

下载 FFmpeg
  1. Windows 11, 64 位环境下, 下载包含 win64 的版本.

其中 gpllgpl 的区别:

  • GPL (GNU General Public License): GPL 许可的 FFmpeg 版本包含了所有功能, 包括一些受专利保护的编解码器, 例如 x264, x265 等等.
  • LGPL (GNU Lesser General Public License): LGPL 许可的 FFmpeg 版本功能相对较少, 它不包含那些受专利保护的编解码器.

**在实际使用中, GPL 版本的功能更强大, 支持的格式更多, 所以我们一般选择 GPL 版本. **

sharedstatic 的区别:

  • Shared: Shared 版本表示动态链接库版本, 它的体积比较小, 运行时需要依赖系统中的一些 DLL 文件.
  • Static: Static 版本表示静态链接库版本, 它的体积比较大, 它将所有依赖的库都打包到了一个可执行文件中, 不需要依赖系统中的 DLL 文件, 可以独立运行.

第一次使用 FFmpeg 下载 Static 版本更方便, 不需要配置环境变量等操作.

比如ffmpeg-n5.1-latest-win64-gpl-5.1.zip 或者 ffmpeg-master-latest-win64-gpl.zip

这两个版本都是 64 位 Windows 系统下的 GPL 许可的静态链接库版本, 功能强大且使用方便. 随便下载哪个都行, master 是最新的开发版本, n5.1-latest 是最新的 5.1 的稳定版本. 对于我们目前的需求, 其实哪个都行.

  1. 将 FFmpeg 的 bin 目录 C:\ffmpeg\bin 添加到系统环境变量中:

添加完环境变量后, 就可以在命令行中直接使用 FFmpeg 命令了. 可以打开命令行, 输入 ffmpeg -version, 如果能够看到 FFmpeg 的版本信息, 就说明配置成功了.

FFmpeg 命令
bash 复制代码
ffmpeg -i video_part.m4s -i audio.mp4 -c:v copy -c:a copy output.mp4

命令说明:

  • ffmpeg: 调用 FFmpeg 程序.
  • -i video_part.m4s: 指定视频输入文件.
  • -i audio.mp4: 指定音频输入文件.
  • -c:v copy: 指定视频编码器为 copy, 表示直接复制视频流, 不进行重新编码.
  • -c:a copy: 指定音频编码器为 copy, 表示直接复制音频流, 不进行重新编码.
  • output.mp4: 指定输出文件名.

命令说明:**

  • ffmpeg: 调用 FFmpeg 程序.
  • -i video_part.m4s: 指定视频输入文件.
  • -i audio.mp4: 指定音频输入文件.
  • -c:v copy: 指定视频编码器为 copy, 表示直接复制视频流, 不进行重新编码.
  • -c:a copy: 指定音频编码器为 copy, 表示直接复制音频流, 不进行重新编码.
  • output.mp4: 指定输出文件名.

这个命令会将 video_part.m4s 和 audio.m4s 合并成一个名为 output.mp4 的文件, 而且是无损合并.

相关推荐
SSH_55232 小时前
【大模型】情绪对话模型项目研发
人工智能·python·语言模型
love530love2 小时前
【笔记】在 MSYS2(MINGW64)中安装 python-maturin 的记录
运维·开发语言·人工智能·windows·笔记·python
仟濹4 小时前
【HTML】基础学习【数据分析全栈攻略:爬虫+处理+可视化+报告】
大数据·前端·爬虫·数据挖掘·数据分析·html
小小小小宇5 小时前
前端WebWorker笔记总结
前端
小狮子安度因5 小时前
关于ffplay在macos上运行奔溃的问题
macos·ffmpeg
G皮T5 小时前
【Python Cookbook】文件与 IO(二)
python·i/o·io·文件·gzip·stringio·bytesio
小小小小宇5 小时前
前端监控用户停留时长
前端
小小小小宇6 小时前
前端性能监控笔记
前端
封奚泽优6 小时前
使用Python绘制节日祝福——以端午节和儿童节为例
人工智能·python·深度学习
干啥都是小小白6 小时前
话题通信之python实现
python·机器人·ros