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

Django 4异步StreamingHTTPResponse流式生成大Zip文件问题求助

Django ASGI模式下大文件流式Zip传输解决方案

问题根源

你的核心问题在于:

  • 标准zipfile.ZipFile无法支持流式生成Zip包——它需要先缓存所有文件的压缩数据和元信息,最后写入中央目录,直接导致内存占用过高;
  • 你的实现没有输出Zip的结束标识,浏览器无法判断流是否完成,最终中断传输;
  • 原WSGI环境下的zipfly依赖同步IO,在ASGI异步环境中会阻塞事件循环,导致方案失效。

解决方案

我们需要手动构建Zip格式的各个组成部分(本地文件头、压缩数据、中央目录、结束记录),实现真正的流式输出;同时使用异步文件IO避免阻塞ASGI事件循环,并且禁用Nginx的响应缓冲确保数据流实时传输到客户端。

实现代码

首先安装异步文件依赖:

pip install aiofiles

然后编写流式Zip生成逻辑:

import os
import zlib
import struct
import aiofiles
from django.http import StreamingHttpResponse

# Zip格式常量定义
ZIP_LOCAL_HEADER_SIGNATURE = 0x04034b50
ZIP_CENTRAL_HEADER_SIGNATURE = 0x02014b50
ZIP_END_CENTRAL_SIGNATURE = 0x06054b50
ZIP_DEFLATED = 8

async def create_zip_streaming_response(files_to_compress: list, filename: str):
    async def generate():
        central_directory = []
        central_dir_offset_start = 0
        total_bytes_written = 0

        for file_path in files_to_compress:
            file_name = os.path.basename(file_path)
            file_size = os.path.getsize(file_path)

            # 计算文件CRC32校验值
            crc = 0
            async with aiofiles.open(file_path, 'rb') as f:
                while chunk := await f.read(65536):
                    crc = zlib.crc32(chunk, crc)
            crc &= 0xFFFFFFFF

            # 分块压缩文件内容
            compressor = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -15)
            compressed_chunks = []
            async with aiofiles.open(file_path, 'rb') as f:
                while chunk := await f.read(65536):
                    compressed_chunk = compressor.compress(chunk)
                    if compressed_chunk:
                        compressed_chunks.append(compressed_chunk)
                compressed_chunk = compressor.flush()
                if compressed_chunk:
                    compressed_chunks.append(compressed_chunk)
            compressed_size = sum(len(c) for c in compressed_chunks)

            # 构造本地文件头并输出
            local_header = struct.pack(
                '<IHHHHIIIIHH',
                ZIP_LOCAL_HEADER_SIGNATURE,
                20,  # 解压所需最低版本
                0,  # 通用位标记
                ZIP_DEFLATED,  # 压缩方式
                0,  # 修改时间(占位)
                0,  # 修改日期(占位)
                crc,
                compressed_size,
                file_size,
                len(file_name),
                0,  # 扩展字段长度
            ) + file_name.encode('utf-8')
            
            yield local_header
            total_bytes_written += len(local_header)
            current_local_offset = total_bytes_written - len(local_header)

            # 输出压缩后的文件数据块
            for chunk in compressed_chunks:
                yield chunk
                total_bytes_written += len(chunk)

            # 记录中央目录条目
            central_record = struct.pack(
                '<IHHHHIIIIHHHHHII',
                ZIP_CENTRAL_HEADER_SIGNATURE,
                20,  # 创建版本
                20,  # 解压所需版本
                0,  # 通用位标记
                ZIP_DEFLATED,  # 压缩方式
                0,  # 修改时间
                0,  # 修改日期
                crc,
                compressed_size,
                file_size,
                len(file_name),
                0,  # 扩展字段长度
                0,  # 文件注释长度
                0,  # 起始磁盘号
                0,  # 内部文件属性
                0o100644 << 16,  # 外部文件属性(Unix权限)
                current_local_offset,  # 本地文件头偏移
            ) + file_name.encode('utf-8')
            central_directory.append(central_record)

        # 输出中央目录
        central_dir_offset_start = total_bytes_written
        central_dir_size = sum(len(r) for r in central_directory)
        for record in central_directory:
            yield record
            total_bytes_written += len(record)

        # 输出Zip结束记录
        end_record = struct.pack(
            '<IHHHHIIH',
            ZIP_END_CENTRAL_SIGNATURE,
            0,  # 当前磁盘号
            0,  # 中央目录起始磁盘号
            len(central_directory),  # 当前磁盘的中央目录条目数
            len(central_directory),  # 总中央目录条目数
            central_dir_size,
            central_dir_offset_start,
            0,  # Zip注释长度
        )
        yield end_record

    response = StreamingHttpResponse(generate(), content_type='application/zip')
    response['Content-Disposition'] = f'attachment; filename="{filename}"'
    # 禁用Nginx缓冲,确保流式传输生效
    response['X-Accel-Buffering'] = 'no'
    return response

关键说明

  1. 真正的流式传输:手动生成Zip的每个结构块,处理完一部分数据就立即yield给客户端,不需要缓存整个Zip包,彻底解决内存占用问题。
  2. 异步IO兼容:使用aiofiles异步读取源文件,避免阻塞ASGI事件循环,保证服务的并发能力。
  3. 完整Zip结构:最后输出中央目录和结束记录,让浏览器能正确识别Zip文件的结束,避免传输中断。
  4. Nginx适配:添加X-Accel-Buffering: no响应头,禁止Nginx缓冲整个响应,否则会回到内存耗尽的问题。

注意事项

  • 分块大小(示例中为65536字节)可根据文件大小调整,过小会增加IO次数,过大则提升内存占用。
  • 如果需要支持中文文件名,需要将通用位标记设置为0x0800(启用UTF-8编码)。
  • Uvicorn部署时,建议调整--limit-concurrency参数控制并发流式请求数,避免内存过载。

内容的提问来源于stack exchange,提问作者T-101

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 17:10:03