脚本地址:
项目地址: Gazer
BiliGrab.py
提要
适用于: 登录状态下, 非大会员视频下载.
自动解析任意 B 站非大会员 / 付费视频的视频 & 音频请求链接并下载, 需要添加 Cookie 保证视频清晰度. 使用 FFmpeg 命令无损合并视频和音频.
使用方法
- 克隆或下载项目代码.
- 安装依赖:
pip install requests
, 或者克隆项目代码后pip install -r requirements.txt
- 脚本顶部: 指定常量
FOLDER_PATH
, 保存视频和音频文件夹路径 - 主函数处: 填写你的
cookie
- 主函数处: 指定
url
, 要下载的视频 URL - 主函数处: 指定
video_file_name
和audio_file_name
, 要保存的视频和音频文件名
代码结构
get_url
从 HTML 中的window.__playinfo__
提取解析视频的 2 个请求 URLdownload_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 requestsvideo_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 对象).
- 在
-
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__
变量的值.
- 原先使用了
正则表达式详解
window\.__playinfo__\s*=\s*({.*?"video":.*?"audio":.*?"session":.*?})\s*
这个正则表达式从 HTML 源代码中提取 B 站视频页面的 window.__playinfo__
变量的值. window.__playinfo__
是一个 JavaScript 变量, 它包含了视频和音频的播放信息(例如 URL、清晰度、编码等), 通常以 JSON 格式存储.
详细解释:
-
window\.__playinfo__
: 匹配字符串 “window.__playinfo__”.window
: 匹配字面上的 “window”.\.
: 匹配一个点号 “.”. 由于点号在正则表达式中有特殊含义(匹配任意字符), 所以需要使用反斜杠\
进行转义.__
: 匹配两个下划线 “_”.playinfo
: 匹配字面上的 “playinfo”
-
\s*
: 匹配零个或多个空白字符(空格、制表符、换行符等). -
=
: 匹配等号 “=”. -
\s*
: 匹配零个或多个空白字符. -
({.*?})
: 这是整个正则表达式的核心, 用于捕获window.__playinfo__
变量的值(JSON 对象).(
和)
: 定义一个捕获组, 用于提取匹配到的内容.{
和}
: 匹配大括号, 表示 JSON 对象的开始和结束..*?
: 匹配任意字符(除了换行符),*?
表示非贪婪模式, 尽可能少地匹配字符..*?"video":
: 匹配任意字符直到"video":
出现..*?"audio":
: 匹配任意字符直到"audio":
出现..*?"session":
: 匹配任意字符直到"session"
出现..*?
: 匹配任意字符直到}
.
-
\s*
: 匹配在结尾大括号后的零个或多个空白字符.
在 Python 中使用(示例):
python">import re
import jsonhtml = """
<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 确保至少可以观看 1080P
目前位置脚本没有加上 Cookie, 如果在自动解析之后仍然不带上 Cookie, 只能下载到 360P 的视频. 下面来确认一下:
1. 未登录状态:
在未登录状态下, B 站只能播放 360P 视频.
2. 参数缺失:
请求 URL 缺少某些参数. 通过比较手动抓取的 URL 和脚本抓取的 URL, 可以发现一些差异.
- 程序抓取的 URL:
https://xy60x163x162x138xy240eyf7ye000y660yy1bxy.mcdn.bilivideo.cn:4483/upgcxcode/99/38/1482513899/1482513899-1-100022.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEqxTEto8BTrNvN0GvT90W5JZMkX_YN0MvXg8gNEV4NC8xNEV4N03eN0B5tZlqNxTEto8BTrNvNeZVuJ10Kj_g2UB02J0mN0B5tZlqNCNEto8BTrNvNC7MTX502C8f2jmMQJ6mqF2fka1mqx6gqj0eN0B599M=&uipk=5&nbs=1&deadline=1740721988&gen=playurlv2&os=mcdn&oi=2104662226&trid=00004af14e75c1fa489794b0c1129a26c41eu&mid=0&platform=pc&og=cos&upsig=d9494a1a248c855fea3d23418a267e22&uparams=e,uipk,nbs,deadline,gen,os,oi,trid,mid,platform,og&mcdnid=50015422&bvc=vod&nettype=0&orderid=0,3&buvid=23327D6A-D1D8-11E7-0808-4DEDFA843F7716556infoc&build=0&f=u_0_0&agrr=0&bw=19818&logo=A0020000
- 手动抓取的 URL (高清):
https://xy124x225x28x109xy.mcdn.bilivideo.cn:8082/v1/resource/1482513899-1-100026.m4s?agrr=1&build=0&buvid=75265G3Y-C2S1-66P3-8396-2FVCDS983Y0925738infoc&bvc=vod&bw=88882&deadline=1740702720&e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEqxTEto8BTrNvN0GvT90W5JZMkX_YN0MvXg8gNEV4NC8xNEV4N03eN0B5tZlqNxTEto8BTrNvNeZVuJ10Kj_g2UB02J0mN0B5tZlqNCNEto8BTrNvNC7MTX502C8f2jmMQJ6mqF2fka1mqx6gqj0eN0B599M%3D&f=u_0_0&gen=playurlv2&logo=A0020000&mcdnid=50016483&mid=73467246&nbs=1&nettype=0&og=cos&oi=2104662226&orderid=0%2C3&os=mcdn&platform=pc&sign=a9b5fe&traceid=tranVDKskSntAK_0_e_N&uipk=5&uparams=e%2Cuipk%2Cnbs%2Cdeadline%2Cgen%2Cos%2Coi%2Ctrid%2Cmid%2Cplatform%2Cog&upsig=caca7e9a425e2dbc2c491a81e9acda17
关键参数差异:
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
.traceid
和sign
: 高清 URL 包含traceid
和sign
, 而程序抓取的 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
- Windows 11, 64 位环境下, 下载包含
win64
的版本.
其中 gpl
和 lgpl
的区别:
- GPL (GNU General Public License): GPL 许可的 FFmpeg 版本包含了所有功能, 包括一些受专利保护的编解码器, 例如 x264, x265 等等.
- LGPL (GNU Lesser General Public License): LGPL 许可的 FFmpeg 版本功能相对较少, 它不包含那些受专利保护的编解码器.
**在实际使用中, GPL 版本的功能更强大, 支持的格式更多, 所以我们一般选择 GPL 版本. **
shared
和 static
的区别:
- 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 的稳定版本. 对于我们目前的需求, 其实哪个都行.
- 将 FFmpeg 的
bin
目录C:\ffmpeg\bin
添加到系统环境变量中:
添加完环境变量后, 就可以在命令行中直接使用 FFmpeg 命令了. 可以打开命令行, 输入 ffmpeg -version
, 如果能够看到 FFmpeg 的版本信息, 就说明配置成功了.
FFmpeg 命令
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 的文件, 而且是无损合并.