B站录播全自动下载归档:从801期视频到123云盘的工程实践
一台京东云免费试用的 2C4G 小水管,一张 6年50TB 的123云盘 VIP,一个 B站大会员 Cookie,一套 Shell + Python 脚本。801期、1.6TB,全自动。
起因
我平时看 B站 UP主「以里illi」的直播录播。久而久之就想:为什么不把 801期全部下载下来收藏?刚好 123云盘充了 6年 50TB VIP,空间管够。
合集地址:https://space.bilibili.com/3035038/lists/1405777?type=season
一、为什么偏偏用"小水管"?
先算一笔带宽账。
801期视频,每期约 2GB(1080P + 音频),总量约 1.6TB。5Mbps 上行带宽,理论每天最多上传:
5Mbps ≈ 0.625MB/s
0.625 × 86400 = 54000MB = 54GB/天一个月 500GB 流量帽,也就能处理 250期。全量需要 3-4 台服务器。
这么慢,为什么不用海外高性能服务器?
123云盘的 CDN 没有海外节点。 WebDAV 上传地址 webdav.123pan.cn 解析到国内 IP。从海外服务器上传,数据要穿越国际链路,实测速度可能连 1Mbps 都稳不住——抖动大、丢包高、TCP 窗口根本打不开。国内 5Mbps 虽然绝对值低,但到123盘的链路是内网级的,rclone 上传全程跑满带宽,实际吞吐量反而碾压海外机。
加上京东云免费试用不花钱,15天一台能跑完约 250期,换三四台就能覆盖全部——整个项目服务器成本为零。
结论:国内小带宽 + 直连云盘 CDN > 海外大带宽 + 跨境上传。
二、B站合集 API:拿到801个 BV号
2.1 接口探索
第一步是获取全部视频的 BV号列表。B站网页端合集页面是瀑布流加载,翻到底才触发下一批请求。直接抓网页不现实。
试过的接口:
最后一个接口是唯一能拿到数据的。请求方式:
GET /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
}
]
}
}2.2 翻页策略
每页最大 page_size=100,共需 9 次请求。但全部走完会被 B站风控(大量同接口高频调用→412)。加了两个措施:
页间停顿 2 秒
每次请求带完整浏览器 UA:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...
最终拿到 801 个 BV号,加工成 bv_list.txt:
BV1514y1Z7ao # 2023.3.26 【以里illi】录播
BV1kg4y1L7ms # 2023.3.27【以里illi】 录播
BV1vg4y1L7oK # 2023.3.29 【以里illi】录播
...三、BBDown:下载引擎
3.1 为什么选 BBDown
B站下载工具有不少选择:you-get、yt-dlp、Bilibili-Evolved(浏览器脚本)、BBDown。选 BBDown 的理由:
TV API 支持:通过
--use-tv-api使用 B站 TV 端 API,限制比 Web 端少、画质可选档位更多大会员 Cookie 透传:
--cookie "SESSDATA=..."解锁 1080P 高码率和 HEVC多 P 自动合并:下载完自动调 ffmpeg 合并音视频轨
单文件 + dotnet runtime:无其他运行时依赖,部署简单
3.2 命令参数
实际使用命令:
bbdown \
--work-dir /root/downloads \ # 输出目录
--use-tv-api true \ # TV端API
--cookie "SESSDATA=..." \ # 大会员身份
"https://www.bilibili.com/video/BV1kg4y1L7ms"Cookie 格式:
BBDown 需要的 cookie 字符串就是浏览器的完整 Cookie 请求头值。核心字段是 SESSDATA:
SESSDATA=a74a9b1a%2C1780847698%2C61a11%2Ac1...%2C 是逗号的 URL 编码。1780847698 是过期时间戳(2026年6月),过期后需从浏览器重新导出。
3.3 画质选择逻辑
BBDown 的输出显示可选档位:
[2026-05-31 18:43:42.325] - 发现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]
[2026-05-31 18:43:42.325] - 发现3个音频流:
0. [M4A] [78 kbps] [~91.35 MB]
1. [M4A] [67 kbps] [~78.47 MB]
2. [M4A] [34 kbps] [~39.82 MB]
[2026-05-31 18:43:42.326] - 自动选择:
[视频] [1080P 高码] [AVC] [1698 kbps] [~1.90 GB]
[音频] [M4A] [78 kbps] [~91.35 MB]BBDown 默认选最高码率。HEVC(H.265)编码需要 UP主上传时选择且大会员专属,多数录播上传时用的是 AVC,所以实际下载的几乎没有 HEVC 流。~1.90 GB 是根据码率 × 时长估算的,视频 02h39m54s × 1698kbps ÷ 8 ≈ 2.03GB,加上音频约 2.1GB 总量。
3.4 下载过程
BBDown 的下载管线:
1. 账号登录验证 → 解析 SESSDATA,获取 access_token
2. 获取 aid (视频ID) → /x/web-interface/view?bvid=BVxxx
3. 获取视频流信息 → /x/player/playurl (TV API, fnver=0, fnval=4048)
4. 选择最优流 → 1080P + M4A
5. 分片下载 → 多连接并发,每个分片 2MB
6. 音视频合并 → 调用 ffmpeg -i video.m4s -i audio.m4s -c copy output.mp4
7. 清理临时分片关键在第3步:TV API 返回的播放地址域名可能是 upos-sz-mirrorcoso1.bilivideo.com(B站 UPOS CDN 的深圳镜像节点),从国内服务器下载延迟 <10ms,带宽能跑到几百 Mbps,下载阶段不是瓶颈。
四、rclone + 123云盘 WebDAV
4.1 rclone 配置
rclone 配置文件 /root/.config/rclone/rclone.conf:
[p123]
type = webdav
url = https://webdav.123pan.cn/webdav
vendor = other
user = “你的手机号”
pass = “123自动生成的密码”vendor = other 很重要——123盘的 WebDAV 实现不是标准的 Apache/Nginx,设成 nextcloud 或 owncloud 会触发额外的 PROPFIND 探测请求导致 405。
4.2 123盘 WebDAV 的坑
地址不是 dav.123.cn: 网上不少教程写的是 dav.123.cn,那是旧版,已废弃。正确地址是 https://webdav.123pan.cn/webdav。
密码不是登录密码: 应用密码需要在 123盘后台 → 账号设置 → 应用密码 → 生成。长度 16 位,全小写字母数字。登录密码无法用于 WebDAV 认证。
大文件上传限制: 123盘 WebDAV 单文件限制 100GB,远超单期视频的 2GB,不是问题。但 rclone 默认的 --transfers 4(4个并发文件)配合单个文件上传没有意义——我们一期内只有一个 mp4。
4.3 上传命令
rclone copy /root/downloads/ p123:/录播/ --progress--progress 输出格式:
Transferred: 512.000 MiB / 1.900 GiB, 26%, 640 KiB/s, ETA 37m20s但在 auto_dl.sh 的上下文里,--progress 的输出是交互式的(用 \r 原地刷新),不适合写入日志。所以实际用了 2>&1 | tail -5 >> "$LOG" 只抓最后 5 行——代价是上传过程中日志看不到进度,只有结束后才知道结果。
上传速度实测:
5Mbps 上行 × 95%利用率 = 4.75Mbps ≈ 594KB/s
2.1GB ÷ 594KB/s ≈ 3700s ≈ 1小时2分钟和监控面板显示的每期约 1 小时吻合。
五、auto_dl.sh:自动化引擎
5.1 进度持久化设计
不引入额外数据库,用 bv_list.txt 本身承载状态。格式:
#DONE BV1514y1Z7ao # 2023.3.26 【以里illi】录播
BV1kg4y1L7ms # 2023.3.27【以里illi】 录播已完成的行在行首加 #DONE 前缀。好处:
统计进度:
grep -c '^#DONE' bv_list.txt纯文本、人可读
用
sed原地修改:sed -i "s/^${BV}/#DONE ${BV}/" "$LIST"rclone copy同步回云端,换机续跑
为什么不用 sed -i 追加而是替换? 因为需要保证 #DONE 只出现在行首,避免误匹配。正则 ^BVxxxx 精确匹配行首的 BV号。
5.2 流量累计算法
/root/bandwidth_used.txt 只存一个整数(MB):
add_bw() {
local cur=$(cat "$BW_FILE")
local new=$((cur + $1))
echo "$new" > "$BW_FILE"
}
# 用完累加
FILE_SIZE=$(du -m "$DOWN_FILE" | cut -f1)
add_bw $((FILE_SIZE * 2)) # 下载+上传约等于两倍文件大小这是粗略估计,没有精确区分下载和上传的实际字节数。但对于每月 500GB 的限额判断来说,误差 ±5% 够用了——495GB 的停止线已经留了 5GB 余量。
5.3 风控对抗
B站的反爬主要表现:
60秒间隔是经验值——试过 30秒,连续 10期后触发 -509;60秒跑了几十期没出问题。
5.4 错误处理与幂等性
脚本的关键幂等保证:
下载失败 → 跳过(不标 #DONE)→ 下轮换机时重试
上传失败 → 跳过(不标 #DONE)→ 保留本地文件,下轮重试
脚本被 kill → 重启后从云端拉回 bv_list.txt → 从上次成功位置继续
换新服务器 → rclone copy 拉回 bv_list.txt → 无缝续跑任何环节失败都不会丢失进度。
六、monitor.py:监控面板
6.1 总体架构
┌─────────────────────────────────────────┐
│ 浏览器 │
│ 每10秒自动刷新 │
│ 壁纸 / 右键菜单 / 暗色模式 │
└──────────────┬──────────────────────────┘
│ HTTP GET :8888
▼
┌─────────────────────────────────────────┐
│ Python HTTPServer │
│ ┌─────────────────────────────────────┐ │
│ │ build_page() │ │
│ │ ├─ get_stats() 读 bv_list.txt │ │
│ │ ├─ parse_log() 读 auto_dl.log │ │
│ │ ├─ calc_speed() 读 /proc/net/dev│ │
│ │ ├─ get_log_html() 日志→HTML │ │
│ │ └─ str.replace() 数据注入模板 │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘
│
▼ 读文件
┌─────────────────────────────────────────┐
│ /proc/net/dev 网卡字节统计 │
│ /root/bv_list.txt 进度文件 │
│ /root/auto_dl.log 下载日志 │
│ /root/downloads/ 当前下载文件大小 │
│ /tmp/monitor_state.json 上次采样快照 │
└─────────────────────────────────────────┘6.2 实时网速采样
这是面板上最核心的实时数据。实现靠两次读取 /proc/net/dev 并计算差值:
def sample_net():
"""读取所有网口的 rx+tx 字节总数"""
total = 0
with open("/proc/net/dev") as f:
for line in f:
if ":" in line:
parts = line.split(":")[1].split()
total += int(parts[0]) # rx bytes (第1列)
total += int(parts[8]) # tx bytes (第9列)
return total
def calc_speed():
now = time.time()
net_now = sample_net()
# 读取上次采样
state = json.load(open(STATE_FILE)) # {"time": ..., "net_bytes": ...}
elapsed = now - state["time"]
net_speed = (net_now - state["net_bytes"]) / elapsed # bytes/s
# 保存本次采样
json.dump({"time": now, "net_bytes": net_now}, open(STATE_FILE, "w"))
return net_speed为什么用 /proc/net/dev 而不是解析 rclone 输出?
rclone 的 --progress 输出用 \r 原地刷新,写入文件后只有最后一行。解析工具输出不可靠。而 /proc/net/dev 是内核维护的统计值,不依赖任何上层工具,只要服务器有网络流量就会被计算。
为什么不区分下载和上传?
/proc/net/dev 按网口统计,不能区分具体是哪个进程的流量。但由于 5Mbps 带宽很小,同一时间只有下载或上传之一在进行(脚本是串行的),所以总流量就是当前活动的方向。
两次采样间隔的影响:
页面 10秒刷新一次,但第一次访问时没有上次采样,速度显示 -。刷新第二次后(10秒间隔),采样间隔 = 10秒,覆盖了 BBDown/rclone 的完整活动周期,速度值基本准确。
6.3 日志语法高亮
BBDown 和 rclone 检测到 stdout 被重定向到文件时,不会输出终端控制码(ANSI escape codes)。所以日志是纯文本,需要基于正则的关键词匹配来上色。
颜色映射:
PATTERNS = {
r'\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?\]': '#6b7280', # 时间戳灰色
r'(成功|完成|DONE|OK)': '#3fb950', # 成功绿色加粗
r'(失败|错误|Error|FAIL)': '#f85149', # 失败红色加粗
r'(BV\w+)': '#d29922', # BV号金色
r'(>>>)': '#56d4dd', # 开始标记青色
r'(下载|上传|同步|清理|标记)': '#58a6ff', # 动作蓝色
r'(开始|等待|暂停|跳过)': '#d29922', # 状态橙色
r'(~[\d.]+ [GM]B)': '#d2a8ff', # 文件大小紫色
r'(\[[^\]]*\d+p[^\]]*\])': '#56d4dd', # 画质信息青色
r'(={3,})': '#52525b', # 分隔线暗灰
}每个正则按顺序执行 re.sub(),匹配到的片段用 <span style="color:..."> 包裹。因为是逐个独立替换,同一个词可能被多个规则命中——比如"下载完成"中的"下载"先被动作规则着色,"完成"再被成功规则着色,两者互不干扰。
Python .format() 引发的 CSS 崩溃:
第一版模板用 .format(**data) 注入数据,但 CSS 里的 {margin:0} 和 JS 里的 function(){...} 包含大量花括号,被当成格式占位符,报 KeyError: 'margin'。先改成 {{ 转义,但八千多字符的模板里漏一个就炸。最终方案:用 %%MARKER%% 双百分号标记 + str.replace() 逐个替换,完全不与 CSS/JS 的花括号冲突。
6.4 前端设计
参考自己个人主页 balong.in 的苹果风格:
毛玻璃效果:
backdrop-filter: blur(30px) saturate(150%)+ 半透明背景圆点纹理:
radial-gradient(rgba(0,0,0,.12) 1px, transparent 1px)铺满背景渐变光晕:多个
radial-gradient叠加模拟环境光壁纸系统:3张背景图轮换,与个人主页共用 123云盘图床
自动昼夜:通过浏览器 Geolocation API 获取经纬度 → 计算当地日出日落时间 → 自动切换亮/暗模式
右键菜单:拦截
contextmenu事件,渲染自定义毛玻璃菜单
七、数据与性能
7.1 单期耗时拆解
以 BV1kg4y1L7ms(2h39m时长,1080P高码率)为例的实际测量:
瓶颈 86% 的时间在上传。
7.2 全网规模估算
801期 × 72分钟/期 = 57672分钟 ≈ 40天(单台串行)
15天服务器有效期 ÷ 72分钟/期 ≈ 250期/台
801期 ÷ 250期/台 ≈ 3.2台
实际需要 4台京东云免费试用机
总流量:1.6TB
123云盘存储占用:1.6TB / 50TB = 3.2%八、部署与运维
8.1 启动流程
# 1. 标记已手工完成的第一期
sed -i 's/^BV1514y1Z7ao/#DONE BV1514y1Z7ao/' /root/bv_list.txt
# 2. 同步进度到云端
rclone copy /root/bv_list.txt p123:/录播/
# 3. 后台启动下载脚本
nohup /root/auto_dl.sh > /root/auto_dl_console.log 2>&1 &
# 4. 后台启动监控面板
nohup python3 /root/monitor.py > /dev/null 2>&1 &
# 5. 查看实时日志(可选)
tail -f /root/auto_dl.log8.2 日常检查命令
tail -20 /root/auto_dl.log # 最近日志
grep -c '#DONE' /root/bv_list.txt # 已完成
grep -v '#DONE' /root/bv_list.txt | head -5 # 接下来5期
cat /root/bandwidth_used.txt # 已用流量MB
ps aux | grep auto_dl # 进程存活8.3 Cookie 过期处理
SESSDATA 有效期约 3-6个月。过期后:
浏览器登录 B站 → F12 → Application → Cookies → 复制
SESSDATA值更新
/root/auto_dl.sh中的COOKIE=行pkill auto_dl && nohup /root/auto_dl.sh > ... &
监控面板观察到连续下载失败 → 第一条日志显示 -799 错误 → 立即排查 Cookie。
8.4 换服务器续跑
# 新机上
rclone copy p123:/录播/bv_list.txt /root/
# 重建 auto_dl.sh 和 monitor.py
# 启动sleep 60 保证了即使两台机器同时跑,也不会对 B站发起高频请求。
九、经验总结
技术决策回顾
可复用的部分
进度文件设计:
#DONE前缀方案极其简单但非常鲁棒,适用于任何串行任务队列流量估算消费:
下载+上传 ≈ 文件大小×2,配合 5%余量,实际运维中从不超标内核级网速监控:
/proc/net/dev采样比任何工具输出解析都可靠云端状态同步:一个
rclone copy实现跨机器状态共享,不需要 Redis
部署体验
这次项目的部署全程通过 Claude Code 的 SSH 能力完成。以前在服务器上开发调试的流程是:本地编辑 → 复制 → SSH 终端粘贴 → 测试 → 报错 → 切回本地改 → 再复制 → 再粘贴。现在 Claude Code 直接连接服务器,写脚本、上传、启动、排错一站式完成,不用碰剪贴板。
2026年5月31日