在Django中提供动态生成的ZIP归档文件

70

如何在Django中为用户提供动态生成的ZIP存档文件?

我正在制作一个网站,用户可以选择可用书籍的任意组合,并将它们下载为ZIP存档文件。我担心为每个请求生成这样的存档文件会使我的服务器变得非常缓慢。我也听说Django目前没有很好的解决方案来提供动态生成的文件。

10个回答

49
解决方案如下。
使用Python模块zipfile创建zip存档文件,但是作为文件指定StringIO对象(ZipFile构造函数需要类似文件的对象)。添加要压缩的文件。然后在Django应用程序中,使用HttpResponse返回StringIO对象的内容,并将mimetype设置为application/x-zip-compressed(或至少application/octet-stream)。如果需要,可以设置content-disposition标头,但这不是真正必需的。
但是要注意,每个请求都创建zip存档文件是个坏主意,这可能会使你的服务器崩溃(如果存档文件很大,则还不包括超时)。从性能的角度来看,最好将生成的输出缓存到文件系统中的某个地方,并且仅在源文件发生更改时重新生成它。更好的想法是提前准备存档文件(例如通过cron作业),并让您的Web服务器像通常一样提供它们作为静态文件。

StringIO将在Python 3.0中被删除,因此您可能希望相应地调整您的代码。 - Jeff Bauer
14
它并没有消失,只是被移到了io模块中。http://docs.python.org/3.0/library/io.html#io.StringIO - Roger Pate
1
只是一个想法,既然您已经手动创建了HttpResponse,那么您不能将其用作缓冲区吗?我的意思是将响应传递给zipfile,并让它直接写入其中。我已经在其他事情上做过了。如果您正在处理大量流,这可能会更快且更节省内存。 - Oli
@Oli 很好,但 ZipFile 需要 f.seek(),而 HttpResponse 不支持。 - dbr

46

以下是一个用Django编写的视图:

import os
import zipfile
import StringIO

from django.http import HttpResponse


def getfiles(request):
    # Files (local path) to put in the .zip
    # FIXME: Change this (get paths from DB etc)
    filenames = ["/tmp/file1.txt", "/tmp/file2.txt"]

    # Folder name in ZIP archive which contains the above files
    # E.g [thearchive.zip]/somefiles/file2.txt
    # FIXME: Set this to something better
    zip_subdir = "somefiles"
    zip_filename = "%s.zip" % zip_subdir

    # Open StringIO to grab in-memory ZIP contents
    s = StringIO.StringIO()

    # The zip compressor
    zf = zipfile.ZipFile(s, "w")

    for fpath in filenames:
        # Calculate path for file in zip
        fdir, fname = os.path.split(fpath)
        zip_path = os.path.join(zip_subdir, fname)

        # Add file, at correct path
        zf.write(fpath, zip_path)

    # Must close zip for all contents to be written
    zf.close()

    # Grab ZIP file from in-memory, make response with correct MIME-type
    resp = HttpResponse(s.getvalue(), mimetype = "application/x-zip-compressed")
    # ..and correct content-disposition
    resp['Content-Disposition'] = 'attachment; filename=%s' % zip_filename

    return resp

2
不需要在这个例子中,但通常请确保内容分发头中的文件名被引用和适当转义。例如,如果文件名中有空格,大多数浏览器将只使用空格之前的部分作为文件名(例如,“attachment; filename=Test File.zip”会被保存为“Test”)。 - Mike DeSimone
@MikeDeSimone 很好的观点。有没有一种好的方法来为这样的上下文转义文件名? - dbr
https://dev59.com/SXVD5IYBdhLWcg3wGHeu - Mike DeSimone
8
对于 Django 版本大于 1.7 的情况,请使用 content_type 代替 mimetype。 - renzop
2
我能用 b = BytesIO.BytesIO() 替换这个来处理二进制文件吗? - qarthandso

36

许多答案建议使用StringIOBytesIO缓冲区。但是,这并不需要,因为HttpResponse已经是一个类似文件的对象:

response = HttpResponse(content_type='application/zip')
zip_file = zipfile.ZipFile(response, 'w')
for filename in filenames:
    zip_file.write(filename)
response['Content-Disposition'] = 'attachment; filename={}'.format(zipfile_name)
return response

请注意,您不应该调用zip_file.close(),因为打开的“文件”是response,我们绝对不希望关闭它。


2
如此简单! - robsco
3
请记得调用zip_file.close()函数。 - chaggy
我认为最佳答案是: - SirSaleh
可能需要 Python 3.5+。如果我没记错的话,我是用 Django 1.11 和 Python 3.5 进行测试的。 - Antoine Pinsard
3
还有一个名为FileResponse的对象。 - djvg
显示剩余2条评论

11

我使用了Django 2.0Python 3.6

import zipfile
import os
from io import BytesIO

def download_zip_file(request):
    filelist = ["path/to/file-11.txt", "path/to/file-22.txt"]

    byte_data = BytesIO()
    zip_file = zipfile.ZipFile(byte_data, "w")

    for file in filelist:
        filename = os.path.basename(os.path.normpath(file))
        zip_file.write(file, filename)
    zip_file.close()

    response = HttpResponse(byte_data.getvalue(), content_type='application/zip')
    response['Content-Disposition'] = 'attachment; filename=files.zip'

    # Print list files in zip_file
    zip_file.printdir()

    return response

嘿,我有同样的目标要完成,但是我不是使用文件列表,而是有多个图像URL需要下载并压缩,然后作为响应输出,你有什么流式传输的想法吗?我的意思是,我有一个工作代码,我只需要使用请求获取图像并将其写入BytesIO,然后再写入zip_file,但如果图像大小很大,则下载时间太长,然后超时。任何帮助都可以。谢谢。 - yashas123
这是一个糟糕的答案。你正在将整个zip文件加载到内存中。想象一下一个10GB的文件。 - sandes
如果你只处理少量小文件,那么这是一个好答案。 - Jotunheim

8

对于Python3,我使用io.ByteIO 代替 StringIO 来实现此功能,因为StringIO已经被弃用。希望有所帮助。

import io

def my_downloadable_zip(request):
    zip_io = io.BytesIO()
    with zipfile.ZipFile(zip_io, mode='w', compression=zipfile.ZIP_DEFLATED) as backup_zip:
        backup_zip.write('file_name_loc_to_zip') # u can also make use of list of filename location
                                                 # and do some iteration over it
     response = HttpResponse(zip_io.getvalue(), content_type='application/x-zip-compressed')
     response['Content-Disposition'] = 'attachment; filename=%s' % 'your_zipfilename' + ".zip"
     response['Content-Length'] = zip_io.tell()
     return response

使用这样的代码,我无法正确命名文件。目前,它只是一个看起来像UUID的随机字符串。 - freethebees

6
Django不直接处理动态内容的生成(特别是Zip文件)。这项工作将由Python标准库完成。您可以查看如何在Python中动态创建Zip文件的方法,请点击此处
如果您担心这会减慢服务器的速度,如果您预计会有许多相同的请求,您可以缓存请求。您可以使用Django的缓存框架来帮助您解决这个问题。
总的来说,压缩文件可能会占用CPU资源,但Django不应该比其他Python Web框架慢。

5

不要介意我打广告:您可以使用django-zipview来实现相同的目的。

pip install django-zipview后:

from zipview.views import BaseZipView

from reviews import Review


class CommentsArchiveView(BaseZipView):
    """Download at once all comments for a review."""

    def get_files(self):
        document_key = self.kwargs.get('document_key')
        reviews = Review.objects \
            .filter(document__document_key=document_key) \
            .exclude(comments__isnull=True)

        return [review.comments.file for review in reviews if review.comments.name]

2

我建议使用单独的模型来存储这些临时的压缩文件。您可以动态创建zip文件,使用filefield保存到模型中,最后将url发送给用户。

优点:

  • 使用django媒体机制(如普通上传)提供静态zip文件。
  • 通过定期cron脚本执行(可以使用zip文件模型中的日期字段)清理过期的zip文件的能力。

0

这个主题已经有很多贡献了,但是当我第一次研究这个问题时,我发现了这个帖子,所以我想加入我的两分钱。

集成自己的zip创建可能不如Web服务器级别的解决方案稳健和优化。同时,我们正在使用Nginx,它没有默认的模块。

然而,您可以使用mod_zip模块编译Nginx(请参见此处获取具有最新稳定版本Nginx的docker镜像,并使用alpine基础使其比默认Nginx镜像更小)。这将添加zip流功能。

然后Django只需要提供要压缩的文件列表,就完成了!使用库来响应此文件列表更具可重用性,django-zip-stream正好提供了这一点。

不幸的是,它从未真正为我工作过,所以我开始了一个分支,进行修复和改进。

您可以在几行代码中使用它:

def download_view(request, name=""):
    from django_zip_stream.responses import FolderZipResponse
    path = settings.STATIC_ROOT
    path = os.path.join(path, name)

    return FolderZipResponse(path)

你需要一种方法让Nginx服务于所有你想要存档的文件,但仅限于此。


-1

你不能只写一个指向“zip服务器”或其他什么的链接吗?为什么zip归档文件本身需要从Django提供服务?在我看来,这里真正需要的只是一个90年代CGI脚本来生成zip并将其输出到stdout。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接