B站录播全自动下载归档:从 801 期视频到 123 云盘的可迁移流水线
一台国内 2C4G 试用服务器,一份 123 云盘大容量空间,一个 B 站账号 Cookie,一套 Shell + Python 监控脚本。801 期、约 1.6TB 的录播归档,本质上不是“下载脚本”,而是一次关于带宽、状态持久化、云盘 WebDAV 和长任务容错的工程实践。
先说边界
本文记录的是一次个人归档场景下的工程实践:如何把一批自己长期观看、需要留档的视频,稳定地下载、上传、续跑和监控。
需要提前说明几件事:
- 不要公开分发归档内容。视频版权、平台协议和 UP 主权益都需要被尊重。
- 不要把 Cookie、SESSDATA、应用密码写进仓库或日志。这些等价于账号凭据,泄露后风险很高。
- 不要把“稳定请求”理解成“对抗平台”。本文的限速、间隔和错误处理,是为了减少失败和避免无意义的重试,不是为了规避平台规则。
- 工具状态会变化。本文使用过的 BBDown 主仓库在 2026 年 5 月已归档,不再维护。部署时必须锁定版本,并准备替代方案。
起因:为什么要做成一条流水线
我平时看 B 站 UP 主「以里 illi」的直播录播。久而久之,产生了一个想法:既然已经看了这么久,为什么不把完整合集做一份长期归档?
合集地址:
https://space.bilibili.com/3035038/lists/1405777?type=season
当时合集一共 801 期。按照每期约 2GB 估算,总量约 1.6TB。手上刚好有一份 123 云盘大容量空间,存储容量不是瓶颈。真正的瓶颈在三件事上:
- B 站列表如何稳定拿到 801 个 BV 号;
- 下载、上传、失败重试、换机续跑如何自动化;
- 京东云试用机的带宽和流量限制如何利用到最大。
所以这不是“写个命令下载视频”那么简单,而是一个小型批处理系统:有任务队列、有状态持久化、有传输层瓶颈、有监控面板,也有失败恢复。
一、为什么不用海外高性能服务器
先算一笔账。
如果单期约 2GB,801 期就是:
801 × 2GB ≈ 1.6TB
京东云试用机上行带宽是 5Mbps,理论上传吞吐:
5Mbps ÷ 8 = 0.625MB/s
0.625 × 86400 ≈ 54000MB ≈ 54GB/天
也就是说,哪怕全天跑满,一天也只能上传约 54GB。15 天试用期理论上限约 810GB,但考虑协议开销、失败重试、机器初始化、Cookie 失效和风控等待,实际一台机器更接近 200~300 期。
直觉上,租一台海外高带宽机器似乎更快。但实际测试后发现,瓶颈不只是“机器带宽”,而是服务器到 123 云盘 WebDAV 节点的链路质量。
123 云盘面向国内用户,WebDAV/上传链路的体验更依赖国内网络环境。海外服务器上传到国内 WebDAV 节点时,跨境链路抖动、丢包和 TCP 拥塞窗口都会影响实际吞吐。相比之下,国内 5Mbps 虽然数字小,但可以稳定跑满。
最终结论是:
这类任务不能只看服务器标称带宽,而要看“源站 → 服务器 → 云盘”的整条路径。下载阶段靠近 B 站 CDN,上传阶段靠近 123 云盘,比单纯堆高配置更重要。
还有一个细节:云厂商的“月流量”统计口径并不完全一致。有的主要计公网出站,有的可能在控制台展示双向总流量。为了保守,本文把单期流量估算为:
单期流量 ≈ 下载文件大小 + 上传文件大小 ≈ 文件大小 × 2
如果你的云厂商只计出站,那么这个估算会偏保守;如果按双向流量或你只是看网卡总流量,它更接近真实消耗。
二、B 站合集列表:先拿到稳定任务队列
2.1 为什么不直接抓网页
B 站合集页面是前端分页/瀑布流加载。直接抓 HTML,拿到的通常不是完整列表,而是首屏数据和一堆异步请求入口。真正可用的数据来自接口请求。
尝试过几类接口:
| 接口 | 结果 |
|---|---|
/x/space/arc/search | 只能搜 UP 主空间投稿,不严格限定合集 |
/x/series/archives | 老接口,兼容性不稳定 |
/x/polymer/web-space/seasons_series_archives_list | 当前场景返回不符合预期 |
/x/polymer/web-space/seasons_archives_list | 能按 season_id 返回合集稿件 |
最终使用的是:
GET https://api.bilibili.com/x/polymer/web-space/seasons_archives_list
?mid=3035038
&season_id=1405777
&page_num=1
&page_size=100
返回结构大致如下:
{
"code": 0,
"data": {
"page": {
"total": 801
},
"archives": [
{
"bvid": "BV1514y1Z7ao",
"title": "2023.3.26 【以里illi】录播",
"duration": 8634,
"cover": "https://...",
"cid": 1113497268
}
]
}
}
这类接口并不是官方开放平台 API,更接近网页端内部接口。字段、路径、鉴权方式和风控限制都有可能随站点改版变化。本文记录的是当时可用的实现方式,复现前应以浏览器实际请求和最新接口记录为准。
2.2 请求节奏与错误处理
每页 page_size=100,801 期一共需要 9 次请求。列表拉取不是高频操作,完全没有必要打满请求。
我的做法是:
- 每页请求间隔 2 秒;
- 带正常浏览器 UA;
- 失败时记录 page_num,不直接丢弃;
- 生成任务文件后人工抽查头尾几项。
最终得到 bv_list.txt:
BV1514y1Z7ao # 2023.3.26 【以里illi】录播
BV1kg4y1L7ms # 2023.3.27 【以里illi】录播
BV1vg4y1L7oK # 2023.3.29 【以里illi】录播
...
这个文件就是后续所有自动化的任务队列。相比一开始就上数据库,纯文本反而更适合这个场景:可读、可 grep、可同步、可手工修复。
三、下载引擎:BBDown 好用,但必须锁版本
3.1 为什么选 BBDown
当时可选工具包括 you-get、yt-dlp、Bilibili-Evolved、BBDown。最终选择 BBDown,主要因为它在 B 站场景下比较直接:
- 支持 BV/AV/EP/SS 等多种输入;
- 支持 Web / TV 等解析模式;
- 支持传入 Cookie;
- 能自动下载多轨并调用 ffmpeg 合并;
- 单文件部署,适合扔到临时服务器上跑批处理。
但这里有一个非常重要的更新:BBDown 主仓库已经归档,不再维护。这改变了它在系统里的定位:它可以作为一次性归档任务中的“固定版本下载器”,但不应该被当成长期可靠、持续维护的通用下载基础设施。
一次性归档:固定版本、固定任务、固定输入 → 可以接受
长期服务:依赖持续可用的解析工具 → 需要替代工具、维护 fork 或迁移预案
3.2 命令参数
最终命令使用更贴近 BBDown 习惯的写法:
BBDown \
-tv \
-c "$BILI_COOKIE" \
--work-dir /root/downloads \
"https://www.bilibili.com/video/BV1kg4y1L7ms"
其中:
-tv/--use-tv-api:使用 TV 端解析模式;-c/--cookie:传入浏览器 Cookie;--work-dir:指定下载目录;BILI_COOKIE:不要写死在脚本里,从.env读取。
.env 示例:
BILI_COOKIE='SESSDATA=xxxxxx; bili_jct=xxxxxx; DedeUserID=xxxxxx'
环境文件权限:
chmod 600 /root/.bili-archive.env
脚本中读取:
set -a
source /root/.bili-archive.env
set +a
不要开启 set -x,否则 Cookie 可能被打印进日志。
3.3 画质选择逻辑
BBDown 会列出可用视频流和音频流,然后自动选择较高档位。一次典型输出类似:
发现 3 个视频流:
0. [1080P 高码] [AVC] [1698 kbps] [~1.90 GB]
1. [720P 高码] [AVC] [946 kbps] [~1.06 GB]
2. [480P 高清] [AVC] [659 kbps] [~754.30 MB]
发现 3 个音频流:
0. [M4A] [78 kbps] [~91.35 MB]
1. [M4A] [67 kbps] [~78.47 MB]
2. [M4A] [34 kbps] [~39.82 MB]
还有一个容易被忽略的点:
“1080P 高码率”不等于“HEVC”。HEVC/H.265 是否存在,取决于源站是否提供对应编码流、账号权限以及工具解析结果。多数历史录播最终拿到的仍然可能是 AVC/H.264。
合并阶段通常是:
ffmpeg -i video.m4s -i audio.m4s -c copy output.mp4
-c copy 是流拷贝,不重编码,所以 CPU 压力很小,耗时主要来自磁盘读写。
四、123 云盘 WebDAV 与 rclone
4.1 rclone 配置
rclone 的 WebDAV 后端需要 URL、用户名、密码和 vendor 类型。123 云盘不是 Nextcloud/ownCloud,因此这里使用 vendor = other,避免 rclone 为特定服务添加额外兼容逻辑。
配置文件:
[p123]
type = webdav
# WebDAV 地址以 123 云盘后台“第三方挂载 / WebDAV 授权管理”展示为准;
# 常见形式可能是 https://webdav.123pan.cn/webdav
# 或带账号 ID 的 https://webdav-{账号ID}.pd1.123pan.cn/webdav
url = https://webdav.123pan.cn/webdav
vendor = other
user = 你的手机号或邮箱
pass = rclone_obscure_后的密码
我用交互方式配置,让 rclone 自动处理密码存储:
rclone config
如果手写配置,至少注意三点:
- WebDAV 地址以 123 云盘后台展示为准,不同账号或不同时期可能看到不同域名;
- WebDAV 密码通常是后台生成的应用密码,不是登录密码;
rclone.conf需要限制权限:
chmod 600 ~/.config/rclone/rclone.conf
4.2 copy、move 与校验
最初使用:
rclone copy /root/downloads/ p123:/录播/ --progress
这个命令能用,但自动化脚本里还可以更精细。
如果一次只上传一个文件,推荐用 copyto,目标路径更明确:
rclone copyto "$DOWN_FILE" "p123:/录播/$REMOTE_NAME" \
--retries 3 \
--low-level-retries 10 \
--stats 30s
上传成功后,再删除本地文件:
rm -f "$DOWN_FILE"
也可以用 rclone move,但我个人更喜欢 copyto + 成功后 rm,因为失败状态更直观:文件还在本地,就说明还没有完成闭环。
--progress 适合手动执行,不适合写日志,因为它会用 \r 原地刷新。长期任务里推荐用:
--stats 30s --stats-one-line
这样日志更容易解析。
五、自动化脚本:核心不是下载,而是状态机
5.1 用文本文件做任务队列
bv_list.txt 同时承担“任务队列”和“进度文件”:
#DONE BV1514y1Z7ao # 2023.3.26 【以里illi】录播
BV1kg4y1L7ms # 2023.3.27 【以里illi】录播
查询进度:
grep -c '^#DONE ' /root/bv_list.txt
grep -v '^#DONE ' /root/bv_list.txt | head -5
这个设计有几个优点:
- 不需要数据库;
- 人类可读,SSH 上直接能修;
- 可以用 rclone 同步到云盘;
- 换服务器时只要拉回这个文件,就能续跑。
但原始写法里如果直接用 sed -i 修改进度文件,脚本被 kill 时有很小概率留下半写状态。更稳的做法是“写临时文件 + 原子替换”:
mark_done() {
local bv="$1"
local tmp
tmp="$(mktemp)"
awk -v target="$bv" '
$1 == target { print "#DONE " $0; next }
{ print }
' "$LIST" > "$tmp"
mv "$tmp" "$LIST"
}
这里的关键是:只有当“下载成功 + 上传成功 + 本地清理完成”之后,才调用 mark_done。
5.2 防止重复启动
长任务最怕不小心开两个进程。两个脚本同时读同一个 bv_list.txt,就可能重复下载、重复上传、重复标记。
最简单的防重入方式是 flock:
LOCK_FILE=/tmp/bili_archive.lock
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
echo "已有任务在运行,退出"
exit 1
fi
这比手写 PID 文件靠谱,因为进程异常退出时,文件锁会自动释放。
5.3 流量估算
脚本里使用:
FILE_SIZE=$(du -m "$DOWN_FILE" | cut -f1)
add_bw $((FILE_SIZE * 2))
这套估算的口径是:
保守估算:下载 + 上传 ≈ 文件大小 × 2
如果云厂商只计出站,实际计费可能更接近“文件大小 × 1”
如果脚本重试频繁,实际消耗会高于估算
更稳的停止线:
LIMIT_MB=500000
STOP_MB=480000
不要等到 495GB 才停,因为云面板统计通常有延迟,rclone/BBDown 失败重试也会额外消耗流量。
5.4 错误处理策略
整个脚本的关键原则只有一句:
失败可以重试,但绝不能把失败标记成成功。
状态机可以写成:
| 阶段 | 成功后动作 | 失败后动作 |
|---|---|---|
| 拉取 BV | 进入下载 | 记录错误,等待后重试 |
| BBDown 下载 | 得到本地 mp4 | 不标 DONE,保留日志 |
| rclone 上传 | 云端出现文件 | 不标 DONE,保留本地文件 |
| 本地清理 | 删除本地 mp4 | 记录警告,但不影响云端结果 |
| 标记 DONE | 同步进度文件 | 下轮继续检查 |
这套逻辑看起来笨,但非常可靠。它没有复杂队列,没有 Redis,没有数据库事务,但通过“只在最后一步标记完成”,天然避免了半成功状态。
六、监控面板:从能看见到能放心
6.1 为什么要自己写面板
这个任务一跑就是十几天。只靠 tail -f 有两个问题:
- 手机上看 SSH 日志不舒服;
- 不知道当前进度、速度、剩余期数和最近错误。
所以我写了一个极简的 Python HTTP 监控面板:
浏览器
↓ HTTP GET :8888
Python ThreadingHTTPServer
├─ 读取 bv_list.txt
├─ 读取 auto_dl.log
├─ 读取 /proc/net/dev
├─ 读取 downloads 当前文件大小
└─ 渲染 HTML
ThreadingHTTPServer 比最基础的 HTTPServer 更适合这个场景:即使某次请求卡住,也不至于让整个面板失去响应。
6.2 /proc/net/dev 的正确使用姿势
原理是两次读取网卡收发字节数,计算差值:
def sample_net():
total = 0
with open("/proc/net/dev") as f:
for line in f:
if ":" not in line:
continue
iface, data = line.split(":", 1)
iface = iface.strip()
if iface == "lo" or iface.startswith(("docker", "br-", "veth")):
continue
parts = data.split()
rx = int(parts[0])
tx = int(parts[8])
total += rx + tx
return total
为什么要排除 lo/docker/veth?
因为监控面板可能跑在有 Docker、内网代理或本机服务的机器上。如果把所有虚拟网卡都统计进去,面板显示的“网速”可能包含容器内部流量、回环流量,导致数值虚高。
更严谨的做法是指定公网网卡名,例如:
ip route get 1.1.1.1
找到实际出口网卡后,只统计那一块网卡。
6.3 面板不要裸奔
监控面板最初监听 :8888。这在测试时很方便,但生产环境不能直接暴露公网。
更稳的方式有三种:
方式一:只监听本地,通过 SSH 隧道访问。
server = ThreadingHTTPServer(("127.0.0.1", 8888), Handler)
本地访问:
ssh -L 8888:127.0.0.1:8888 root@server_ip
然后打开:
http://127.0.0.1:8888
方式二:安全组只允许自己的 IP 访问 8888。
适合临时查看,但换网络后要更新白名单。
方式三:前面套 Nginx Basic Auth。
适合长期运行,但配置量会多一点。
无论哪种方式,都不要让一个无认证、包含日志和文件路径的监控面板裸露在公网。
6.4 前端效果的小坑
这个面板有几个挺有趣的设计:
- 毛玻璃背景;
- 暗色模式;
- 壁纸轮换;
- 日志语法高亮;
- 右键菜单;
- 自动昼夜切换。
其中两个工程细节最值得记录。
第一个是模板注入。不要用 .format(**data) 渲染包含大量 CSS/JS 的 HTML 模板,因为 CSS 和 JS 里到处都是 {},很容易和 Python 格式化占位符冲突。更稳的是:
html = TEMPLATE
html = html.replace("%%DONE%%", str(done))
html = html.replace("%%TOTAL%%", str(total))
html = html.replace("%%LOG%%", log_html)
第二个是浏览器 Geolocation API。它通常要求 HTTPS 或 localhost 这样的安全上下文。如果面板是 http://服务器IP:8888,定位权限可能直接不可用。因此自动昼夜切换最好提供降级逻辑:定位失败时按服务器时区或浏览器本地时间切换。
七、性能数据:瓶颈其实只有一个
以一期 2 小时 40 分钟左右的 1080P 高码率录播为例:
| 阶段 | 耗时 | 说明 |
|---|---|---|
| BBDown 初始化 | ~5 秒 | 登录验证、解析稿件、获取播放地址 |
| 视频下载 | ~8 分钟 | B 站 CDN 到国内服务器,入站不是瓶颈 |
| 音视频合并 | ~30 秒 | ffmpeg -c copy,不重编码 |
| 上传到 123 云盘 | ~62 分钟 | 5Mbps 上行成为绝对瓶颈 |
| 间隔等待 | 60 秒 | 降低无意义重试和请求压力 |
| 合计 | 约 72 分钟 | 单台串行处理 |
可以看到,上传占了绝大多数时间:
62 ÷ 72 ≈ 86%
这解释了为什么“换高性能 CPU”没有意义。CPU 只在合并阶段短暂工作,绝大多数时间都卡在 5Mbps 上行链路。
规模估算:
801 期 × 72 分钟/期 ≈ 57672 分钟 ≈ 40 天
15 天/台 ÷ 72 分钟/期 ≈ 300 期理论值
考虑失败、初始化、空窗和流量停止线:约 200~250 期/台
801 期 ÷ 250 期/台 ≈ 3.2 台
实际准备 4 台试用机更稳
这组数据很好地说明了一个关键事实:项目的核心不是“下载工具多快”,而是“在有限带宽下如何让机器持续做有效功”。
八、部署与换机续跑
8.1 目录结构
最终目录结构:
/root/bili-archive/
├── auto_dl.sh
├── monitor.py
├── bv_list.txt
├── bandwidth_used.txt
├── auto_dl.log
├── downloads/
└── .env # Cookie / WebDAV 等敏感配置,不入库
8.2 启动流程
cd /root/bili-archive
# 1. 拉取最新任务状态
rclone copyto p123:/录播/bv_list.txt ./bv_list.txt
# 2. 启动下载归档脚本
nohup ./auto_dl.sh > ./auto_dl_console.log 2>&1 &
# 3. 启动监控面板
nohup python3 ./monitor.py > ./monitor_console.log 2>&1 &
8.3 日常检查命令
tail -20 /root/bili-archive/auto_dl.log
grep -c '^#DONE ' /root/bili-archive/bv_list.txt
grep -v '^#DONE ' /root/bili-archive/bv_list.txt | head -5
cat /root/bili-archive/bandwidth_used.txt
pgrep -af auto_dl
pgrep -af monitor.py
8.4 Cookie 过期处理
SESSDATA 有有效期。过期后通常会表现为解析失败、鉴权失败或工具返回登录相关错误。
处理流程:
- 浏览器重新登录 B 站;
- 开发者工具复制 Cookie;
- 更新
/root/bili-archive/.env; - 重启脚本;
- 观察连续 1~2 期是否恢复正常。
不要把旧 Cookie 留在历史日志里,也不要把 .env 同步到公开仓库。
8.5 换服务器续跑
换机的关键只有三个文件:
bv_list.txt
bandwidth_used.txt
.env
其中 .env 不上传云盘,最好手工重新创建。bv_list.txt 和 bandwidth_used.txt 可以同步到 123 云盘:
rclone copyto ./bv_list.txt p123:/录播/bv_list.txt
rclone copyto ./bandwidth_used.txt p123:/录播/bandwidth_used.txt
新机器上:
mkdir -p /root/bili-archive/downloads
cd /root/bili-archive
rclone copyto p123:/录播/bv_list.txt ./bv_list.txt
rclone copyto p123:/录播/bandwidth_used.txt ./bandwidth_used.txt
然后重新部署脚本即可。
九、这次真正可复用的经验
9.1 技术决策回顾
| 决策 | 当时的理由 | 修订后的补充 |
|---|---|---|
| 国内低带宽服务器 | 到 123 云盘链路更稳 | 重点不是机房配置,而是端到端路径 |
| BBDown | B 站支持更直接 | 主仓库已归档,必须锁版本并准备替代 |
| rclone WebDAV | 跨平台、日志清楚 | vendor=other 更适合非 Nextcloud/ownCloud 服务 |
| 文本队列 | 简单、人可读、易同步 | 加上原子写入和 flock 后更稳 |
/proc/net/dev | 内核统计,不依赖工具输出 | 要排除虚拟网卡或指定公网网卡 |
| Python 监控面板 | 快速可视化 | 不要裸露公网,优先 SSH 隧道 |
9.2 最有价值的模式
第一,文本文件状态机。
#DONE 前缀看起来很土,但它解决了最核心的问题:任务是否完成一眼可见,跨机器迁移不需要任何服务端依赖。
第二,只在闭环完成后标记成功。
下载成功不算成功,上传成功也不一定算成功。只有当云端文件存在、本地清理完成、进度同步成功之后,才标记 #DONE。这个原则比脚本写得多漂亮更重要。
第三,把链路质量作为技术选型的一部分。
“海外高带宽”并不天然优于“国内低带宽”。如果目标云盘在国内,跨境上传可能抵消掉所有带宽优势。
第四,监控不是锦上添花。
长任务没有监控,就只能凭感觉判断它是否还活着。一个很简陋的面板,也能显著降低运维焦虑。
第五,工具时效性必须提前说明。
BBDown 曾经很好用,但仓库归档后,它就不能再被当成稳定长期依赖。对于这类依赖第三方解析的项目,版本、日期和替代方案都要提前写清楚。
结语
这次归档任务表面上是在下载 801 期 B 站录播,实际上练的是一套更通用的工程能力:
- 如何把一次性命令变成可恢复的流水线;
- 如何在低带宽、限流量环境下做批处理;
- 如何用最少的组件完成状态持久化;
- 如何让一个十几天的任务能被观察、能被迁移、能在失败后继续。
最后沉淀下来的经验不是“某个工具怎么用”,而是:面对长周期任务,可靠性往往来自朴素设计——单一任务队列、原子状态更新、保守流量估算、失败不标成功、监控可见。
这套方法以后换成别的下载源、别的云盘、别的批处理任务,也依然能复用。
参考资料
- BBDown GitHub 仓库归档说明:https://github.com/nilaoda/BBDown
- BBDown Issues 页面归档状态:https://github.com/nilaoda/BBDown/issues
- rclone WebDAV 后端文档:https://rclone.org/webdav/
- 123 云盘官网:https://www.123pan.com/
- Linux Kernel 网络接口统计文档:https://www.kernel.org/doc/html/v6.14/networking/statistics.html
- Linux man-pages:
/proc/net/dev格式说明:https://man7.org/linux/man-pages/man5/proc_pid_net.5.html - bilibili-API-collect 野生接口文档:https://github.com/pskdje/bilibili-API-collect
- 哔哩哔哩用户使用协议:https://www.bilibili.com/protocal/licence.html
2026 年 3 月初稿 | 2026 年 4 月技术修订