从零设计一个多平台视频下载器:架构、模块与踩坑指南
市面上有 yt-dlp、VidDown、各类解析网站……你有没有好奇过,一个"粘贴链接 → 下载视频"的工具,背后是什么样的架构?本文以 VidDown 的实际经验为基础,从零拆解一个多平台视频下载器的完整技术架构——包括解析层、下载引擎、调度队列和前端交互。
TL;DR:多平台下载器 = 平台适配器层(各站点解析逻辑) + 下载引擎层(断点续传/并发分片/限速) + 任务调度层(队列/优先级/重试) + Web 层(API + 实时进度推送)。核心难点不在"下载",而在"解析"——每个平台的反爬策略都在持续升级。
目录
- 一、整体架构:四层模型
- 二、解析适配器层:每个平台一个"驱动"
- 三、下载引擎层:断点续传 + 并发分片
- 四、任务调度层:队列、优先级与重试
- 五、Web 层:实时进度推送
- 六、安全与合规设计
- 七、常见踩坑记录
- 八、合规与温馨提示
一、整体架构:四层模型
┌─────────────────────────────────────────┐
│ Web 层 │
│ API(REST)+ WebSocket(实时进度) │
│ 用户粘贴链接 → 提交任务 → 轮询/推送进度 │
└─────────────────────────────────────────┘
│
┌─────────────────────────────────────────┐
│ 任务调度层 │
│ Redis / DB 队列 → Worker 池 → 优先级 │
│ 并发控制、失败重试、超时取消 │
└─────────────────────────────────────────┘
│
┌─────────────────────────────────────────┐
│ 下载引擎层 │
│ HTTP Range 分片 → 并发下载 → 合并校验 │
│ 断点续传、限速、重定向跟随、Cookie 注入 │
└─────────────────────────────────────────┘
│
┌─────────────────────────────────────────┐
│ 解析适配器层 │
│ 每个平台一个 Adapter: │
│ B站 / 抖音 / 快手 / 小红书 / YouTube... │
│ 输入:分享链接 → 输出:真实视频直链 │
└─────────────────────────────────────────┘
这四层各司其职,解耦后可以独立迭代:比如 B 站改了签名算法,只改解析层;要加 WebSocket 推送,只改 Web 层。
二、解析适配器层:每个平台一个"驱动"
这是整个系统最复杂、最常变的一层。核心思路是适配器模式——定义一个统一的接口,每个平台实现自己的解析逻辑:
from abc import ABC, abstractmethod
class BaseParser(ABC):
"""所有平台解析器的基类"""
@abstractmethod
def match(self, url: str) -> bool:
"""判断这个 URL 是不是该平台能处理的"""
pass
@abstractmethod
def parse(self, url: str) -> dict:
"""解析链接,返回视频信息"""
# 返回格式:
# {
# "title": "视频标题",
# "video_url": "https://cdn.xxx.com/video.mp4",
# "audio_url": "https://cdn.xxx.com/audio.mp4", # 可选
# "headers": {"Referer": "...", "User-Agent": "..."},
# "cookies": {"sessionid": "..."},
# "duration": 120,
# "cover": "https://...jpg",
# }
pass
class BilibiliParser(BaseParser):
def match(self, url):
return 'bilibili.com' in url or 'b23.tv' in url
def parse(self, url):
# 1. 短链还原
# 2. 提取 BV 号
# 3. cid → playurl 接口(WBI 签名)
# 4. 取 dash video/audio URL
# 5. 带 Referer + UA 返回
...
关键设计原则:
- match() 要快:只做域名匹配,不要在这里发网络请求;
- parse() 返回标准字典:不管哪个平台,返回格式统一,下载引擎不需要知道平台细节;
- Cookie 和 Headers 外置:让用户可以注入自己的登录态,降低服务器反爬风险。
三、下载引擎层:断点续传 + 并发分片
拿到直链后,下载引擎负责把文件拉回来。核心能力:
import aiohttp
import asyncio
import hashlib
class DownloadEngine:
def __init__(self, url, headers, chunk_size=1024*1024, max_concurrent=4):
self.url = url
self.headers = headers
self.chunk_size = chunk_size # 每片 1MB
self.max_concurrent = max_concurrent
async def get_file_size(self):
async with aiohttp.ClientSession() as session:
async with session.head(self.url, headers=self.headers) as resp:
return int(resp.headers.get('Content-Length', 0))
async def download_chunk(self, session, start, end, part_num):
headers = {**self.headers, 'Range': f'bytes={start}-{end}'}
async with session.get(self.url, headers=headers) as resp:
data = await resp.read()
return part_num, start, data
async def download(self, output_path):
file_size = await self.get_file_size()
if file_size == 0:
# 不支持 Range,退化为单线程下载
...
# 分片任务
tasks = []
chunk_size = file_size // self.max_concurrent
async with aiohttp.ClientSession() as session:
for i in range(self.max_concurrent):
start = i * chunk_size
end = start + chunk_size - 1 if i < self.max_concurrent - 1 else file_size - 1
tasks.append(self.download_chunk(session, start, end, i))
parts = await asyncio.gather(*tasks)
# 按顺序合并
parts.sort(key=lambda x: x[0])
with open(output_path, 'wb') as f:
for _, _, data in parts:
f.write(data)
# 校验(可选)
...
更多细节见 大文件下载总中断?聊聊断点续传与并发分片下载的工程实现。
四、任务调度层:队列、优先级与重试
当多个用户同时提交下载任务时,需要一个调度层:
import redis
import json
from enum import Enum
class TaskStatus(Enum):
PENDING = 'pending'
DOWNLOADING = 'downloading'
MERGING = 'merging'
COMPLETED = 'completed'
FAILED = 'failed'
class TaskScheduler:
def __init__(self):
self.redis = redis.Redis(host='localhost', port=6379, db=0)
def submit(self, task_data):
task_id = generate_task_id()
self.redis.hset(f'task:{task_id}', mapping={
'status': TaskStatus.PENDING.value,
'url': task_data['url'],
'progress': 0,
'created_at': time.time(),
})
self.redis.lpush('task_queue', task_id)
return task_id
def get_next_task(self):
task_id = self.redis.rpop('task_queue')
if task_id:
self.redis.hset(f'task:{task_id}', 'status', TaskStatus.DOWNLOADING.value)
return task_id
def update_progress(self, task_id, progress):
self.redis.hset(f'task:{task_id}', 'progress', progress)
def retry_failed(self, max_retries=3):
# 扫描所有 failed 任务,重试未超过上限的
...
关键设计考量:
- 并发上限:Worker 池大小限制(避免打满带宽)
- 优先级:VIP 用户优先、小文件优先
- 超时控制:单个任务超时自动标记失败
- 持久化:Redis 做队列 + DB 做持久存储,重启不丢任务
五、Web 层:实时进度推送
用户提交链接后,需要看到实时进度。最佳方案是 WebSocket:
# Django Channels / FastAPI WebSocket
from fastapi import FastAPI, WebSocket
app = FastAPI()
@app.websocket("/ws/task/{task_id}")
async def task_progress(websocket: WebSocket, task_id: str):
await websocket.accept()
while True:
progress = redis.hget(f'task:{task_id}', 'progress')
status = redis.hget(f'task:{task_id}', 'status')
await websocket.send_json({
'task_id': task_id,
'progress': int(progress or 0),
'status': status,
})
if status in ('completed', 'failed'):
break
await asyncio.sleep(0.5)
API 设计建议:
POST /api/task/ ← 提交下载任务
GET /api/task/{id}/ ← 查询任务状态
WS /ws/task/{id}/ ← 实时进度推送
GET /api/task/{id}/file ← 下载完成后的文件
六、安全与合规设计
| 层面 | 措施 |
|---|---|
| 速率限制 | 单 IP 每分钟最多 N 个任务 |
| 文件校验 | 下载完成后校验 MD5/SHA256 |
| 内容扫描 | 禁止下载违法内容(关键词过滤) |
| 日志脱敏 | 不记录用户的 Cookie/Token |
| 文件过期 | 服务器上的文件定期清理(24h) |
| 用户协议 | 明确告知"仅限个人备份" |
七、常见踩坑记录
- 大文件 OOM:不要整个文件读进内存再写磁盘,必须流式写入;
- 连接池耗尽:aiohttp 的
ClientSession要复用,不要每次请求 new 一个; - DNS 缓存:CDN 域名可能解析到不同 IP,别用系统 DNS 缓存太久;
- Windows 文件名:标题里有
\/:*?"<>|等非法字符,需要过滤; - GBK 编码:部分国内老 CDN 的 Content-Disposition 用 GBK 编码文件名,需要正确解码。
八、合规与温馨提示
技术是中性的,用法见人心。我们强烈建议:
- 仅下载你自己拥有版权或平台明确允许保存的内容;
- 不将下载器用于盗版传播、商业侵权;
- 尊重创作者与平台服务条款;
- 下载内容请用于个人学习、备份与离线观看。
设计一个多平台视频下载器,本质是解决"怎么从别人的服务器上,合法、高效、稳定地把文件取回来"。解析层是变化的(平台天天改),下载层是稳定的(HTTP 协议不变),调度层是通用的(队列模式万金油)。分层解耦后,加一个新平台只需要写一个 Adapter,其他三层都不用动——这就是好的架构。
本文由 VidDown 技术博客原创发布。VidDown 是一个免费、本地优先的在线视频解析与开发者工具站,支持多平台视频下载、格式转换、m3u8 合并等实用功能,所有数据处理均在本地完成,保护你的隐私。欢迎访问 www.viddown.cn 体验。