Django临时目录存图生成zip下载报错排查及实现
Django实现图片打包ZIP下载的报错排查与实现方案
报错根因说明
IOError: [Errno 21] Is a directory: '/home/project/images':触发原因是代码直接调用open()操作目录路径。open()仅支持对文件执行读写,无法直接打开目录;同时初始代码存在两处本质逻辑错误:一是将图片URL字符串直接写入文件,并未存储实际图片二进制内容;二是直接将图片URL作为本地路径传入zipfile.write(),压缩模块无法读取不存在的本地文件。OSError: [Errno 2] No such file or directory: '0':触发原因是遍历目录获取到的仅为文件名,未拼接目录的完整绝对路径时,压缩模块会在程序运行的当前工作目录下查找对应文件,无法定位到临时目录内的存储文件;同时该版本代码依然存在直接写入URL字符串、未保存实际图片内容的问题。AttributeError: 'HttpResponse' object has no attribute 'seek':触发原因是Python内置zipfile.ZipFile默认要求传入支持随机读写(即实现seek方法)的类文件对象,而Django的HttpResponse是单向输出的流对象,不支持随机读写,直接传入作为压缩包写入目标就会抛出该错误。
正确实现逻辑
完整流程分为5步:
- 创建临时存储目录,配置目录已存在的兼容逻辑,避免重复创建报错
- 遍历图片列表,区分网络URL/本地文件路径,读取图片实际二进制内容写入临时目录,文件保留对应图片后缀
- 生成ZIP压缩包时,不要直接写入
HttpResponse对象,选择临时磁盘文件或内存字节流作为压缩包写入载体 - 压缩完成后读取压缩包二进制内容,写入HttpResponse返回,同时配置正确的下载响应头
- 流程结束后自动清理临时目录、临时压缩包,避免服务器残留垃圾文件
磁盘临时文件版本实现(兼容大文件场景)
import os import zipfile import shutil from django.http import HttpResponse # 处理网络图片时需要引入requests,仅处理本地图片可注释该行 import requests # 替换为项目实际的BASE_DIR配置 from django.conf import settings BASE_DIR = settings.BASE_DIR def download_images_zip(request): # 替换为实际的图片地址列表,支持本地绝对路径、http/https网络URL images_list = [image_absolute_url1, image_absolute_url2] temp_folder = os.path.join(BASE_DIR, "temp_images_store") # 创建临时目录,exist_ok=True兼容目录已存在的场景 os.makedirs(temp_folder, exist_ok=True) temp_zip_path = os.path.join(BASE_DIR, "temp_pack.zip") try: # 写入所有图片到临时目录 for idx, img_path in enumerate(images_list): # 提取图片后缀,过滤URL参数 if "." in img_path: file_suffix = img_path.split(".")[-1].split("?")[0] else: file_suffix = "jpg" save_file_path = os.path.join(temp_folder, f"图片_{idx+1}.{file_suffix}") # 读取图片二进制内容 if img_path.startswith(("http://", "https://")): img_resp = requests.get(img_path, timeout=15) img_content = img_resp.content else: with open(img_path, "rb") as img_f: img_content = img_f.read() # 写入临时文件 with open(save_file_path, "wb") as f: f.write(img_content) # 生成ZIP压缩包 with zipfile.ZipFile(temp_zip_path, "w", zipfile.ZIP_DEFLATED) as zip_f: for filename in os.listdir(temp_folder): file_full_path = os.path.join(temp_folder, filename) # 第二个参数指定压缩包内的文件名,避免打包本地绝对路径 zip_f.write(file_full_path, filename) # 读取压缩包返回响应 with open(temp_zip_path, "rb") as zip_f: response = HttpResponse(zip_f.read(), content_type="application/zip") # 配置下载头,指定用户下载时的默认文件名 response["Content-Disposition"] = 'attachment; filename="图片打包.zip"' return response finally: # 无论流程成功或报错,都清理临时文件 if os.path.exists(temp_folder): shutil.rmtree(temp_folder) if os.path.exists(temp_zip_path): os.remove(temp_zip_path)
内存流版本实现(小文件场景性能更优)
如果图片总大小不超过100M,可直接在内存中完成压缩,减少磁盘IO开销,只需替换上述代码中生成ZIP和返回响应的部分即可:
from io import BytesIO # 图片写入临时目录的逻辑和上述版本一致,省略重复部分 # 生成ZIP时使用内存字节流作为载体 buffer = BytesIO() with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zip_f: for filename in os.listdir(temp_folder): file_full_path = os.path.join(temp_folder, filename) zip_f.write(file_full_path, filename) # 将流指针移动到起始位置,才能读取完整内容 buffer.seek(0) response = HttpResponse(buffer.read(), content_type="application/zip") response["Content-Disposition"] = 'attachment; filename="图片打包.zip"'
注意事项
- 所有图片、压缩包的读写操作都要使用二进制模式(
rb/wb),避免文件内容编码错误导致图片损坏、压缩包无法打开 - 写入压缩包时必须指定第二个参数(压缩包内的文件名),否则会将服务器本地的完整绝对路径打包进ZIP,用户解压后会看到多层嵌套的系统目录
- 临时文件清理逻辑必须放在*
finally块*中执行,避免接口报错时临时文件残留,长期运行占满服务器磁盘 - 处理网络图片时要加超时时间、异常捕获,避免单个图片下载失败导致整个接口报错
内容的提问来源于stack exchange,提问作者D_P
相关产品推荐
相关产品推荐

