提示

返回博客列表

从零设计一个多平台视频下载器:架构、模块与踩坑指南

从零设计一个多平台视频下载器:架构、模块与踩坑指南

市面上有 yt-dlp、VidDown、各类解析网站……你有没有好奇过,一个"粘贴链接 → 下载视频"的工具,背后是什么样的架构?本文以 VidDown 的实际经验为基础,从零拆解一个多平台视频下载器的完整技术架构——包括解析层、下载引擎、调度队列和前端交互。

TL;DR:多平台下载器 = 平台适配器层(各站点解析逻辑) + 下载引擎层(断点续传/并发分片/限速) + 任务调度层(队列/优先级/重试) + Web 层(API + 实时进度推送)。核心难点不在"下载",而在"解析"——每个平台的反爬策略都在持续升级。

目录

一、整体架构:四层模型

┌─────────────────────────────────────────┐
│                 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)
用户协议 明确告知"仅限个人备份"

七、常见踩坑记录

  1. 大文件 OOM:不要整个文件读进内存再写磁盘,必须流式写入;
  2. 连接池耗尽:aiohttp 的 ClientSession 要复用,不要每次请求 new 一个;
  3. DNS 缓存:CDN 域名可能解析到不同 IP,别用系统 DNS 缓存太久;
  4. Windows 文件名:标题里有 \/:*?"<>| 等非法字符,需要过滤;
  5. GBK 编码:部分国内老 CDN 的 Content-Disposition 用 GBK 编码文件名,需要正确解码。

八、合规与温馨提示

技术是中性的,用法见人心。我们强烈建议:

  • 仅下载你自己拥有版权平台明确允许保存的内容;
  • 不将下载器用于盗版传播、商业侵权;
  • 尊重创作者与平台服务条款;
  • 下载内容请用于个人学习、备份与离线观看

设计一个多平台视频下载器,本质是解决"怎么从别人的服务器上,合法、高效、稳定地把文件取回来"。解析层是变化的(平台天天改),下载层是稳定的(HTTP 协议不变),调度层是通用的(队列模式万金油)。分层解耦后,加一个新平台只需要写一个 Adapter,其他三层都不用动——这就是好的架构。

本文由 VidDown 技术博客原创发布。VidDown 是一个免费、本地优先的在线视频解析与开发者工具站,支持多平台视频下载、格式转换、m3u8 合并等实用功能,所有数据处理均在本地完成,保护你的隐私。欢迎访问 www.viddown.cn 体验。

想亲手试试?用 VidDown 一键解析下载

粘贴视频链接即可解析,多平台支持、网页端即用;下载桌面客户端解锁海外平台本地解析,开通会员更享不限次下载。

顶部