B站视频批量下载工具
工具是另一种财富。
概述
在 B 站遇到好的视频总想下载下来。不过很多浏览器插件只能每次下一个,批量下载才省力。有了 AI 的辅助,其实编写小工具已经非常方便了。 不过我们还是需要对生成的工具有要求的。
(1) 命令行选项;(2)可复用。(3)可组合
这其实是从 Linux 系统得来的启示。 命令行,就是可以通过命令行灵活定制行为,可复用,就是每个工具是小的但是可以复用的,当需要时不必重写或修改;可组合,是说命令行可以组合起来实现不同的工具。当我们把任务仔细拆分之后,就能更容易达到这个目标。
要批量下载视频,需要三个工具和一个数据源:
(1)数据解析
- JSON PATH 语法
- HTML select 语法
(2)数据拼接
- URL 拼接
(3)下载工具
- you-get
- ffmpeg
(4) 数据源
- HTML 网页
- API 返回的 JSON 数据。
本文主要讲述如何解析 API 返回的 JSON 数据,拼接成所需的 URL,然后用下载工具下载。
工具开发
数据解析
比如说,要下载个人收藏的视频。
打开B站个人收藏,查看哪个URL 返回了页面数据。

API 返回的 JSON 数据如下:这里面 bvid 就是B站视频的标识,加上前缀即是可访问的B站视频地址。
{
"code": 0,
"message": "0",
"ttl": 1,
"data": {
"info": {
"id": 3133010051,
"fid": 31330100,
"mid": 183260251,
"attr": 22,
"title": "文艺",
"cover": "http://i2.hdslb.com/bfs/archive/843055b47a4cc46e80047ab9087b4bea06491f47.jpg",
"upper": {
"mid": 183260251,
"name": "琴水玉",
"face": "https://i1.hdslb.com/bfs/face/fd135f95e066ea357969cac54468c0273baea6a1.jpg",
"followed": false,
"vip_type": 2,
"vip_statue": 1
},
"cover_type": 2,
"cnt_info": {
"collect": 0,
"play": 0,
"thumb_up": 0,
"share": 0
},
"type": 11,
"intro": "",
"ctime": 1709329450,
"mtime": 1709329450,
"state": 0,
"fav_state": 0,
"like_state": 0,
"media_count": 26,
"is_top": false
},
"medias": [{
"id": 113892432876371,
"type": 2,
"title": "中式美学:沙鸥径去鱼儿饱,野鸟相呼柿子红。",
"cover": "http://i2.hdslb.com/bfs/archive/843055b47a4cc46e80047ab9087b4bea06491f47.jpg",
"intro": "中式美学:沙鸥径去鱼儿饱,野鸟相呼柿子红。",
"page": 1,
"duration": 99,
"upper": {
"mid": 1580957455,
"name": "中式美学分享",
"face": "https://i1.hdslb.com/bfs/face/f8e2fec9c18501d4e577bd7f60030f97c2c8fe54.jpg",
"jump_link": ""
},
"attr": 0,
"cnt_info": {
"collect": 5961,
"play": 104689,
"danmaku": 52,
"vt": 0,
"play_switch": 0,
"reply": 0,
"view_text_1": "10.5万"
},
"link": "bilibili://video/113892432876371",
"ctime": 1737861192,
"pubtime": 1737861192,
"fav_time": 1738047373,
"bv_id": "BV1bvFAe2Efz",
"bvid": "BV1bvFAe2Efz",
"season": null,
"ogv": null,
"ugc": {
"first_cid": 28085587803
},
"media_list_link": "bilibili://music/playlist/playpage/3316270251?page_type=3\u0026oid=113892432876371\u0026otype=2"
}
]
}
}
请 AI 写一个解析 JSON 文件的工具,能够指定路径来获取指定数据。
import json
import jmespath
def get_value_by_path(data, path):
"""通过路径获取JSON中的值
Args:
data (dict): JSON数据
path (str): JsonPath表达式,如 'data.result[?result_type=='video'].data[]'
Returns:
list: 返回所有匹配的值的列表
"""
try:
# 解析并执行JsonPath表达式
print(path)
result = jmespath.search(path, data)
return result
except Exception as e:
print(f"错误: 无法从路径 '{path}' 获取值: {str(e)}")
return []
def extract_values(json_file, path):
"""从指定的JSON文件中提取指定路径的值
Args:
json_file (str): JSON文件的路径
path (str): 以点号分隔的路径
Returns:
list: 包含所有匹配值的列表
"""
try:
# 读取JSON文件
with open(json_file, 'r', encoding='utf-8') as f:
data = json.load(f)
# 获取指定路径的值
values = get_value_by_path(data, path)
# 确保返回值是列表
if not isinstance(values, list):
values = [values]
return values
except FileNotFoundError:
print(f"错误: 找不到文件 '{json_file}'")
return []
except json.JSONDecodeError:
print(f"错误: '{json_file}' 不是有效的JSON文件")
return []
import json
import argparse
from pytools.tools.dw_video import download_video
from pytools.common.jsonparse import extract_values
# ------------------------------------------------------------
# up 主视频
# bf -f video.json -p 'data.list.vlist[*].bvid'
# ------------------------------------------------------------
def genurl(bvids):
urls = []
for bvid in bvids:
urls.append("https://www.bilibili.com/video/" + bvid)
return urls
def main():
# 设置命令行参数
parser = argparse.ArgumentParser(description='从JSON文件中提取指定路径的值')
parser.add_argument('-f', '--file', required=True, help='JSON文件路径')
parser.add_argument('-p', '--path', required=True, help='以点号分隔的JSON路径,例如:data.list.vlist[*].bvid')
args = parser.parse_args()
# 提取值
values = extract_values(args.file, args.path)
print(values)
# 打印结果
if values:
print("\n".join(str(v) for v in values))
# 如果提取的是bvid,则下载视频
urls = genurl(values)
for url in urls:
download_video(url)
if __name__ == '__main__':
main()
AI 很快就写出来了。只要执行 python3 bf.py -f video.json -p "data.list.vlist[*].bvid" 就可以生成该页的所有视频的 B 站网址。这里用到了 JsonPath 语法。可以去学习下:““JsonPath简明教程”。须知软件工程师要十八般武艺样样会一点。
为什么写成这样呢,因为这个是可以复用的。
咱们再来看搜索个人UP主视频返回什么格式。她的 bvid 藏在 data.list.vlist[*].bvid 里。
可以使用 python3 bf.py -f video.json -p "data.list.vlist[*].bvid"
{
"code": 0,
"message": "0",
"ttl": 1,
"data": {
"list": {
"slist": [],
"tlist": {
"160": {
"tid": 160,
"count": 2,
"name": "生活"
},
"36": {
"tid": 36,
"count": 140,
"name": "知识"
}
},
"vlist": [{
"comment": 4,
"typeid": 228,
"play": 1688,
"pic": "http://i2.hdslb.com/bfs/archive/2bf4a4f7ed75aa137c2d999998db6d0123481ad2.jpg",
"subtitle": "",
"description": "中式美学:正浪吟、不觉回桡,水花风叶两悠悠。",
"copyright": "1",
"title": "中式美学:正浪吟、不觉回桡,水花风叶两悠悠。",
"review": 0,
"author": "中式美学分享",
"mid": 1580957455,
"created": 1747611930,
"length": "02:10",
"video_review": 1,
"aid": 114531426636976,
"bvid": "BV1kAESzuEXc",
"hide_click": false,
"is_pay": 0,
"is_union_video": 0,
"is_steins_gate": 0,
"is_live_playback": 0,
"is_lesson_video": 0,
"is_lesson_finished": 0,
"lesson_update_info": "",
"jump_url": "",
"meta": null,
"is_avoided": 0,
"season_id": 0,
"attribute": 8405120,
"is_charging_arc": false,
"elec_arc_type": 0,
"elec_arc_badge": "",
"vt": 0,
"enable_vt": 0,
"vt_display": "",
"playback_position": 0,
"is_self_view": false
},
{
"comment": 1,
"typeid": 228,
"play": 2122,
"pic": "http://i2.hdslb.com/bfs/archive/b5da93822dd42959ef4103b944ad089593cbc928.jpg",
"subtitle": "",
"description": "中式美学:落日熔金,暮云合璧,人在何处。染柳烟浓,吹梅笛怨,春意知几许。",
"copyright": "1",
"title": "中式美学:落日熔金,暮云合璧,人在何处。染柳烟浓,吹梅笛怨,春意知几许。",
"review": 0,
"author": "中式美学分享",
"mid": 1580957455,
"created": 1747434610,
"length": "01:40",
"video_review": 3,
"aid": 114519833580541,
"bvid": "BV1YSE8zKE4Q",
"hide_click": false,
"is_pay": 0,
"is_union_video": 0,
"is_steins_gate": 0,
"is_live_playback": 0,
"is_lesson_video": 0,
"is_lesson_finished": 0,
"lesson_update_info": "",
"jump_url": "",
"meta": null,
"is_avoided": 0,
"season_id": 0,
"attribute": 8405120,
"is_charging_arc": false,
"elec_arc_type": 0,
"elec_arc_badge": "",
"vt": 0,
"enable_vt": 0,
"vt_display": "",
"playback_position": 0,
"is_self_view": false
}
}
}
这里可以对 python3 bf.py 做了一个 alias ,在 ~/.zshrc 里添加
alias bf="python3 /Users/qinshu/tools/pytools/pytools/tools/bf.py"
添加到Path里,然后 source ~/.zshrc
就可以直接使用 bf -f video.json -p "data.list.vlist[*].bvid",这就是可复用的威力,不需要改代码,就可以适应不同的变化。
读者还可以去看看根据关键字搜索B站视频的JSON返回数据,可以用 bss -f video.json -p 'data.result[?result_type=="video"].data[]'来实现。可谓一个程序能解决三种场景。
视频下载
#!/usr/bin/env python3
import subprocess
import shlex
from pathlib import Path
from typing import Optional, Union
import time
def download_video(
video_url: str,
output_dir: Union[str, Path] = Path.cwd(),
timeout: int = 3600, # 1小时超时
retries: int = 3,
verbose: bool = True
) -> Optional[Path]:
"""
使用 y 命令下载视频
参数:
video_url: 视频URL (e.g. "https://www.bilibili.com/video/BV1xx411x7xx")
output_dir: 输出目录 (默认当前目录)
timeout: 超时时间(秒)
retries: 重试次数
verbose: 显示下载进度
返回:
成功时返回下载的视频路径,失败返回None
"""
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
cmd = f"y {shlex.quote(video_url)}"
if verbose:
print(f"开始下载: {video_url}")
print(f"保存到: {output_dir.resolve()}")
print(f"执行命令: {cmd}")
for attempt in range(1, retries + 1):
try:
start_time = time.time()
# 使用Popen实现实时输出
process = subprocess.Popen(
cmd,
shell=True,
cwd=str(output_dir),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
bufsize=1
)
# 实时打印输出
while True:
output = process.stdout.readline()
if output == '' and process.poll() is not None:
break
if output and verbose:
print(output.strip())
# 检查超时
if time.time() - start_time > timeout:
process.terminate()
raise subprocess.TimeoutExpired(cmd, timeout)
# 检查返回码
if process.returncode == 0:
if verbose:
print(f"下载成功 (尝试 {attempt}/{retries})")
return _find_downloaded_file(output_dir, video_url)
else:
raise subprocess.CalledProcessError(process.returncode, cmd)
except (subprocess.TimeoutExpired, subprocess.CalledProcessError) as e:
if attempt < retries:
wait_time = min(attempt * 10, 60) # 指数退避
if verbose:
print(f"尝试 {attempt}/{retries} 失败,{wait_time}秒后重试...")
print(f"错误: {str(e)}")
time.sleep(wait_time)
else:
if verbose:
print(f"下载失败: {str(e)}")
return None
def _find_downloaded_file(directory: Path, video_url: str) -> Optional[Path]:
"""尝试自动查找下载的文件"""
# 这里可以根据实际y命令的输出文件名模式进行调整
# 示例:查找最近修改的视频文件
video_files = sorted(
directory.glob("*.mp4"),
key=lambda f: f.stat().st_mtime,
reverse=True
)
return video_files[0] if video_files else None
这里采用的是直接调用命令行,命令行 y 是如下文件。 加上 chmod +x y
--cookies 里的文件,是firefox 里用 Cookie-Editor 导出的数据复制进去的。如果你有其它的下载工具,也可以替换为自己的下载工具。这里咱们就不单独开发下载工具了。
link=$1
you-get $link --cookies "/Users/qinshu/privateqin/cookies.txt" -f -o /Users/qinshu/joy/dance/bili

获取网络数据
其实还有一步,获取网络数据源,可以通过 python requests 库实现。不过B站加了风控校验,导致我之前的方法失效了。乐趣减少了很多。只能算做了一半工作。后续还要看看怎么突破网站防线拉取数据。
小结
工程师不比程序员,他需要的是十八般武艺样样都会一点,这样会获得更大的自由度。虽然学艺不精,那有什么关系呢?什么问题用什么工具最适合解决。对于手里没钱的码农,多写一点工具让自己的生活更轻松,未尝不是一种选择呢。

浙公网安备 33010602011771号