提示

返回博客列表

自己写一个支持断点续传的分块下载器(Python 实战)

自己写一个支持断点续传的分块下载器(Python 实战)

市面上的下载器(aria2、迅雷、IDM)已经很强,但有些场景你得自己撸一个:比如要在自家服务里内嵌下载能力、要按字节精确控制进度、要对接特殊的鉴权/签名逻辑、或者要在受限环境里跑。这篇文章用 Python 从零写一个支持 HTTP Range 分块、多线程、断点续传、校验完整性的下载器,把原理和坑一次讲透。

一、HTTP Range 是什么

普通 GET 一次拿全部内容。加上 Range 请求头后,可以只取其中一段:

GET /big.mp4 HTTP/1.1
Host: example.com
Range: bytes=0-1023

服务端回 206 Partial Content,只返回第 0–1023 字节。这就是分块下载和断点续传的底层基础。

二、最朴素的单段下载

import requests

url = 'https://example.com/big.mp4'
r = requests.get(url, stream=True)
with open('out.mp4', 'wb') as f:
    for chunk in r.iter_content(8192):
        f.write(chunk)

缺点:不支持断点,中途断了得从头再来;单连接速度受单 TCP 限制。

三、先探测服务器能力

head = requests.head(url, timeout=10)
accepts = head.headers.get('Accept-Ranges') == 'bytes'
size = int(head.headers.get('Content-Length', 0))
  • Accept-Ranges: bytes 表示支持分块;
  • Content-Length 给出总大小;
  • 若不支持 Range,只能退化成单段下载。

四、分块多线程下载

把文件按大小切成 N 块,每块一个线程下到自己那一段,最后落进同一个预分配文件:

import requests, os
from concurrent.futures import ThreadPoolExecutor

def download_chunk(url, start, end, path):
    headers = {'Range': f'bytes={start}-{end}'}
    r = requests.get(url, headers=headers, stream=True, timeout=30)
    with open(path, 'r+b') as f:
        f.seek(start)
        for chunk in r.iter_content(8192):
            f.write(chunk)

def multi_download(url, path, threads=8):
    size = int(requests.head(url, timeout=10).headers['Content-Length'])
    open(path, 'wb').close()
    with open(path, 'r+b') as f:
        f.truncate(size)          # 预分配空间,避免并发写越界
    step = size // threads
    tasks = []
    for i in range(threads):
        s = i * step
        e = size - 1 if i == threads - 1 else s + step - 1
        tasks.append((s, e))
    with ThreadPoolExecutor(max_workers=threads) as ex:
        for s, e in tasks:
            ex.submit(download_chunk, url, s, e, path)

multi_download('https://example.com/big.mp4', 'out.mp4', 8)

关键点:每块写不同偏移(f.seek),范围不重叠,并发写同一个文件是安全的。

五、断点续传:记录已下字节

核心是维护一个进度文件(比如 .prog),记录每个块下到哪了。重启时跳过已完成部分:

import json, os

def resume_download(url, path, threads=8):
    prog = path + '.prog'
    size = int(requests.head(url, timeout=10).headers['Content-Length'])
    if not os.path.exists(path):
        open(path, 'wb').close()
        with open(path, 'r+b') as f:
            f.truncate(size)
        done = {i: 0 for i in range(threads)}
    else:
        done = json.load(open(prog))   # 已下载字节数,按块记录
    # 对每个块,Range 从已下字节开始接着下
    ...
    json.dump(done, open(prog, 'w'))

完整实现按块粒度记录偏移,重启时 Range: bytes=start+downloaded- 接着下。下载完删掉 .prog

六、健壮性:重试、限速、校验

生产级下载器还要考虑:

# 失败重试
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1))
def get_chunk(url, headers):
    return requests.get(url, headers=headers, stream=True, timeout=30)

# 限速(避免打爆带宽)
import time
def throttle(gen, bytes_per_sec):
    bucket = bytes_per_sec
    for chunk in gen:
        yield chunk
        bucket -= len(chunk)
        if bucket <= 0:
            time.sleep(1); bucket = bytes_per_sec

# 完整性校验
import hashlib
def sha256(path):
    h = hashlib.sha256()
    with open(path, 'rb') as f:
        for b in iter(lambda: f.read(65536), b''):
            h.update(b)
    return h.hexdigest()

七、和现成工具对比

维度 自写 aria2 / curl -C
可控性 完全自定义 高但受接口限制
开发成本
速度 取决于实现 极优(连接优化、磁盘调度)
特殊需求 易内嵌 需外部调用
断点续传 自己实现 原生支持

结论:一般需求直接调 aria2c -x 16 -s 16 URLcurl -C - URL;要内嵌进自家服务、或对接特殊逻辑才自写。

八、常见翻车现场

现象 根因 解法
多线程写乱 偏移范围重叠 严格计算每块 [start,end) 不重叠
服务端不支持 Range 返回 200 而非 206 退化单段下载
速度上不去 单 IP 被限流 上代理/多出口
文件损坏 某块缺失/重叠 下完校验大小或 hash
断点失效 没记录偏移 维护进度文件

九、常见问题

Q:多线程写同一文件安全吗? 只要每块写不同偏移且不重叠,就安全;预分配 truncate 避免末尾越界。

Q:Range 的 end 能省略吗? 能,bytes=1000- 表示从 1000 到末尾,适合断点续传的"接着下"。

Q:为什么还要自己写? 内嵌进服务、要自定义鉴权头、要做进度回调 UI、要适配私有协议时,现成工具接入成本高。

Q:能用异步(aiohttp)吗? 能,IO 密集型用 asyncio 更省资源,思路一样:按块发 Range 请求,写各自偏移。

小结

分块下载器三要素:Range 请求头拿片段、预分配文件 + 按偏移并发写、进度文件实现断点续传。自写适合内嵌场景,普通下载直接用 aria2/curl 更省心。

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

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

顶部