You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python threading并行下载文件内容异常问题求助

并行下载文件内容不一致的修复方案

核心问题分析与修复点

1. 锁的作用域错误

原代码中每个DownloadPart实例持有独立的锁,无法实现多线程对同一文件操作的互斥。多个线程同时写入文件时,会导致文件指针混乱、内容覆盖,最终文件损坏。

修复:将锁改为全局共享,在Downloader中创建一个锁,传递给所有DownloadPart实例使用。

2. 文件未预分配空间

在部分操作系统(如Windows)中,直接向文件的偏移位置写入数据不会自动填充空白区域,导致文件实际大小小于预期,后续读取时内容缺失。

修复:打开文件后立即调用file.truncate(file_size)预分配与目标文件大小一致的空间。

3. tqdm进度条非线程安全

tqdm的update方法不是线程安全的,多线程同时调用会导致进度计数错误(虽不影响文件内容,但会导致进度显示异常)。

修复:将进度条的更新操作也放在锁的保护范围内。

4. 增加请求错误检测

原代码未对下载请求的响应状态做校验,若服务器返回错误状态码(如Range请求不被支持),仍会继续写入无效内容。

修复:添加response.raise_for_status()检测请求是否成功。

修复后的完整代码

import requests
import threading
from tqdm import tqdm

DOWNLOAD_CHUNK_SIZE = 1 << 20  # 1 MiB

class DownloadPart:
    def __init__(self, url, byte_range, global_lock) -> None:
        self.url = url
        self.byte_range = byte_range
        self.global_lock = global_lock  # 使用全局共享锁

    def download(self, file, pbar=None):
        start, end = self.byte_range
        response = requests.get(
            self.url,
            headers={"Range": f"bytes={start}-{end}"},
            allow_redirects=True,
            stream=True,
        )
        response.raise_for_status()  # 检测请求错误

        written = 0
        for chunk in response.iter_content(chunk_size=DOWNLOAD_CHUNK_SIZE):
            if chunk:
                with self.global_lock:  # 用with自动管理锁,避免遗漏释放
                    file.seek(start + written)
                    length = file.write(chunk)
                    file.flush()
                    written += length
                    pbar.update(length)


class Downloader:
    def __init__(self, url, parts=10):
        self.url = url
        self.parts = parts
        self.global_lock = threading.Lock()  # 创建全局互斥锁

    def _get_file_size(self) -> int:
        # 先尝试HEAD请求获取文件大小
        info = requests.head(self.url, allow_redirects=True)
        try:
            info.raise_for_status()
            size = info.headers.get("content-length")
            if size:
                return int(size)
        except requests.exceptions.RequestException:
            pass
        
        # HEAD失败时改用GET请求头获取
        with requests.get(self.url, stream=True, allow_redirects=True) as resp:
            resp.raise_for_status()
            size = resp.headers.get("content-length")
            assert size, "无法获取目标文件大小"
            return int(size)

    def download(self, filename):
        file_size = self._get_file_size()
        size_per_part = file_size // self.parts

        print(f"文件总大小: {file_size} 字节, 每个分片大小: {size_per_part} 字节")

        # 用with语句管理文件,自动关闭
        with open(filename, "wb") as file:
            file.truncate(file_size)  # 预分配文件空间
            pbar = tqdm(total=file_size, unit="B", unit_scale=True)

            threads = []
            for index in range(self.parts):
                start = size_per_part * index
                # 最后一个分片覆盖剩余所有字节
                end = file_size - 1 if index == self.parts - 1 else size_per_part * (index + 1) - 1
                part = DownloadPart(self.url, (start, end), self.global_lock)
                thread = threading.Thread(target=part.download, args=(file, pbar))
                thread.start()
                threads.append(thread)

            # 等待所有线程完成
            for thread in threads:
                thread.join()

            pbar.close()


# 测试URL
URL = "https://s-delivery38.mxdcontent.net/v/8a5f59673042ed97c402be84ceeb20d9.mp4?s=TfiDzO2oBLrhub_GhToCiQ&e=1676489987&_t=1676476332"

d = Downloader(URL)
d.download("video.mp4")

内容的提问来源于stack exchange,提问作者Jurakin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 23:05:27