B站录播全自动下载归档:从801期视频到123云盘的工程实践

发表于
6

一台京东云免费试用的 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站网页端合集页面是瀑布流加载,翻到底才触发下一批请求。直接抓网页不现实。

试过的接口:

接口

结果

/x/space/arc/search

只能搜 UP主空间视频,不区分合集

/x/series/archives

老合集接口,已废弃

/x/polymer/web-space/seasons_series_archives_list

返回空

/x/polymer/web-space/seasons_archives_list

可用

最后一个接口是唯一能拿到数据的。请求方式:

 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,设成 nextcloudowncloud 会触发额外的 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站的反爬主要表现:

错误码

含义

触发条件

解法

412

Precondition Failed

无 UA头或 UA 异常

带完整浏览器 UA

-799

未登录/鉴权失败

无 Cookie 或过期

带 SESSDATA

-509

请求过于频繁

短时间内大量请求

每期间隔60秒

-404

视频不存在

BV号错误或已删除

跳过继续

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高码率)为例的实际测量:

阶段

耗时

说明

BBDown 初始化

~5秒

登录验证 + 获取播放地址

视频下载

~8分钟

1.9GB ÷ 约4MB/s(B站CDN不限速)

音视频合并

~30秒

ffmpeg -c copy 流拷贝,不重编码

上传到123盘

~62分钟

2.1GB ÷ 约594KB/s(5Mbps瓶颈)

等待间隔

1分钟

防风控

合计

~72分钟

瓶颈 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.log

8.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                          # 进程存活

SESSDATA 有效期约 3-6个月。过期后:

  1. 浏览器登录 B站 → F12 → Application → Cookies → 复制 SESSDATA

  2. 更新 /root/auto_dl.sh 中的 COOKIE=

  3. 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站发起高频请求。


九、经验总结

技术决策回顾

决策

为什么

国内低带宽 vs 海外高带宽

123盘 CDN 国内直连 > 跨境

BBDown vs you-get/yt-dlp

TV API 画质更好、多P合并、单文件

纯 Shell 脚本 vs Python

更轻量,核心逻辑就一个 while 循环

文本文件做进度 vs 数据库

零依赖、人可读、rclone 可同步

/proc/net/dev vs 解析工具输出

内核统计,不依赖任何进程

str.replace() vs .format()

避开 CSS/JS 花括号冲突

可复用的部分

  1. 进度文件设计#DONE 前缀方案极其简单但非常鲁棒,适用于任何串行任务队列

  2. 流量估算消费下载+上传 ≈ 文件大小×2,配合 5%余量,实际运维中从不超标

  3. 内核级网速监控/proc/net/dev 采样比任何工具输出解析都可靠

  4. 云端状态同步:一个 rclone copy 实现跨机器状态共享,不需要 Redis

部署体验

这次项目的部署全程通过 Claude Code 的 SSH 能力完成。以前在服务器上开发调试的流程是:本地编辑 → 复制 → SSH 终端粘贴 → 测试 → 报错 → 切回本地改 → 再复制 → 再粘贴。现在 Claude Code 直接连接服务器,写脚本、上传、启动、排错一站式完成,不用碰剪贴板。


2026年5月31日