目录

B站录播全自动下载归档:从 801 期视频到 123 云盘的可迁移流水线

26

一台国内 2C4G 试用服务器,一份 123 云盘大容量空间,一个 B 站账号 Cookie,一套 Shell + Python 监控脚本。801 期、约 1.6TB 的录播归档,本质上不是“下载脚本”,而是一次关于带宽、状态持久化、云盘 WebDAV 和长任务容错的工程实践。


先说边界

本文记录的是一次个人归档场景下的工程实践:如何把一批自己长期观看、需要留档的视频,稳定地下载、上传、续跑和监控。

需要提前说明几件事:

  1. 不要公开分发归档内容。视频版权、平台协议和 UP 主权益都需要被尊重。
  2. 不要把 Cookie、SESSDATA、应用密码写进仓库或日志。这些等价于账号凭据,泄露后风险很高。
  3. 不要把“稳定请求”理解成“对抗平台”。本文的限速、间隔和错误处理,是为了减少失败和避免无意义的重试,不是为了规避平台规则。
  4. 工具状态会变化。本文使用过的 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

如果手写配置,至少注意三点:

  1. WebDAV 地址以 123 云盘后台展示为准,不同账号或不同时期可能看到不同域名;
  2. WebDAV 密码通常是后台生成的应用密码,不是登录密码;
  3. 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 有两个问题:

  1. 手机上看 SSH 日志不舒服;
  2. 不知道当前进度、速度、剩余期数和最近错误。

所以我写了一个极简的 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

SESSDATA 有有效期。过期后通常会表现为解析失败、鉴权失败或工具返回登录相关错误。

处理流程:

  1. 浏览器重新登录 B 站;
  2. 开发者工具复制 Cookie;
  3. 更新 /root/bili-archive/.env
  4. 重启脚本;
  5. 观察连续 1~2 期是否恢复正常。

不要把旧 Cookie 留在历史日志里,也不要把 .env 同步到公开仓库。

8.5 换服务器续跑

换机的关键只有三个文件:

bv_list.txt
bandwidth_used.txt
.env

其中 .env 不上传云盘,最好手工重新创建。bv_list.txtbandwidth_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 云盘链路更稳重点不是机房配置,而是端到端路径
BBDownB 站支持更直接主仓库已归档,必须锁版本并准备替代
rclone WebDAV跨平台、日志清楚vendor=other 更适合非 Nextcloud/ownCloud 服务
文本队列简单、人可读、易同步加上原子写入和 flock 后更稳
/proc/net/dev内核统计,不依赖工具输出要排除虚拟网卡或指定公网网卡
Python 监控面板快速可视化不要裸露公网,优先 SSH 隧道

9.2 最有价值的模式

第一,文本文件状态机。

#DONE 前缀看起来很土,但它解决了最核心的问题:任务是否完成一眼可见,跨机器迁移不需要任何服务端依赖。

第二,只在闭环完成后标记成功。

下载成功不算成功,上传成功也不一定算成功。只有当云端文件存在、本地清理完成、进度同步成功之后,才标记 #DONE。这个原则比脚本写得多漂亮更重要。

第三,把链路质量作为技术选型的一部分。

“海外高带宽”并不天然优于“国内低带宽”。如果目标云盘在国内,跨境上传可能抵消掉所有带宽优势。

第四,监控不是锦上添花。

长任务没有监控,就只能凭感觉判断它是否还活着。一个很简陋的面板,也能显著降低运维焦虑。

第五,工具时效性必须提前说明。

BBDown 曾经很好用,但仓库归档后,它就不能再被当成稳定长期依赖。对于这类依赖第三方解析的项目,版本、日期和替代方案都要提前写清楚。


结语

这次归档任务表面上是在下载 801 期 B 站录播,实际上练的是一套更通用的工程能力:

  • 如何把一次性命令变成可恢复的流水线;
  • 如何在低带宽、限流量环境下做批处理;
  • 如何用最少的组件完成状态持久化;
  • 如何让一个十几天的任务能被观察、能被迁移、能在失败后继续。

最后沉淀下来的经验不是“某个工具怎么用”,而是:面对长周期任务,可靠性往往来自朴素设计——单一任务队列、原子状态更新、保守流量估算、失败不标成功、监控可见。

这套方法以后换成别的下载源、别的云盘、别的批处理任务,也依然能复用。


参考资料


2026 年 3 月初稿 | 2026 年 4 月技术修订