Python 创建并流式传输大型存档,而无需将其存储在内存或磁盘上

Python 创建并流式传输大型存档,而无需将其存储在内存或磁盘上,python,http,Python,Http,我想允许用户一次下载多个大文件的存档。但是,文件和归档文件可能太大,无法存储在内存中或我的服务器上的磁盘上(它们是从其他服务器动态传入的)。我希望在将归档文件流式传输给用户时生成归档文件 我可以用柏油或拉链或任何最简单的东西。我正在使用django,它允许我在响应中返回生成器或类似文件的对象。这个物体可以用来推动这个过程。但是,我很难弄清楚如何围绕zipfile或tarfile库构建这类库,我担心它们可能不支持边读边读文件,或者在构建归档文件时阅读归档文件 这个答案可能会有所帮助tarfile#

我想允许用户一次下载多个大文件的存档。但是,文件和归档文件可能太大,无法存储在内存中或我的服务器上的磁盘上(它们是从其他服务器动态传入的)。我希望在将归档文件流式传输给用户时生成归档文件

我可以用柏油或拉链或任何最简单的东西。我正在使用django,它允许我在响应中返回生成器或类似文件的对象。这个物体可以用来推动这个过程。但是,我很难弄清楚如何围绕zipfile或tarfile库构建这类库,我担心它们可能不支持边读边读文件,或者在构建归档文件时阅读归档文件


这个答案可能会有所帮助
tarfile#addfile
需要一个iterable,但它似乎会立即将其传递给
shutil.copyfileobj
,因此这可能不像我希望的那样对生成器友好。

您可以通过生成并流式传输zip文件来实现,而无需压缩,基本上就是在每个文件的内容之前添加标题。你是对的,这些库不支持这个,但是你可以绕过它们来让它工作

这段代码使用一个类包装zipfile.zipfile,该类管理流,并为文件创建zipfile.ZipInfo实例。可以在末尾设置CRC和大小。您可以使用put_file()、write()和flush()将输入流中的数据推入其中,并使用read()将其中的数据读取到输出流中

导入结构
进口拉链
导入时间
从StringIO导入StringIO
类ZipStreamer(对象):
定义初始化(自):
self.out_stream=StringIO()
#在不压缩的情况下写入stringIO
self.zipfile=zipfile.zipfile(self.out_流'w',zipfile.ZIP_存储)
self.current\u文件=无
self.\u last\u streamed=0
def put_文件(self、name、date_time=None):
如果日期\时间为无:
date\u time=time.localtime(time.time())[:6]
zinfo=zipfile.ZipInfo(名称、日期和时间)
zinfo.compress_type=zipfile.ZIP_存储
zinfo.flag_位=0x08

zinfo.external_attr=0600我最终使用了。

您可以将ZipFile流式传输到Pylons或Django响应文件obj,方法是将文件obj包装在实现
tell()
的文件中。这将在内存中的zip中缓冲每个单独的文件,但流式传输zip本身。我们使用它来流式下载一个装满图像的zip文件,所以我们在内存中不会缓冲超过一个图像

此示例流式传输到
sys.stdout
。对于挂架,使用
response.body_文件
,对于Django,可以将
HttpResponse
本身作为文件使用

import zipfile
import sys


class StreamFile(object):
    def __init__(self, fileobj):
        self.fileobj = fileobj
        self.pos = 0

    def write(self, str):
        self.fileobj.write(str)
        self.pos += len(str)

    def tell(self):
        return self.pos

    def flush(self):
        self.fileobj.flush()


# Wrap a stream so ZipFile can use it
out = StreamFile(sys.stdout)
z = zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED)

for i in range(5):
    z.writestr("hello{0}.txt".format(i), "this is hello{0} contents\n".format(i) * 3)

z.close()

以下是Pedro Werneck(上图)的解决方案,但有一个避免在内存中收集所有数据的修复程序(
read
方法有点固定):

例如:


一般来说,压缩实用程序(如zip或tar)需要读取整个输入文件,以确定哪些内容可以压缩,哪些内容应该压缩。所以我认为你的建筑理念是有缺陷的。@jedwards:完全错误
tar
只是一个容器,没有压缩。它被设计成与磁带一起工作——在这里,首先阅读整个东西是不可能的。而
zlib
将愉快地压缩数据流。您可以通过完全的文件感知获得更好的压缩,但这绝不是强制性的。@sarnold,我认为他指的是压缩的tarball,因为他在谈论压缩。zlib在生成输出之前仍然需要缓存大量的输入字节,因为仍然需要分析输入数据。因此,我同意我错误地指出了“完整”,但我仍然认为这没有什么意义,因为压缩小流段所节省的数据量与编写所花费的工作量相比微不足道。@jedwards,响应越细微越好,但是,如果数据是来自无线电天线或麦克风的话,那么就可能有足够的机会来存储流数据,这种格式更便于传输和解包,而不需要先存储所有的数据进行分析。压缩可以由apachemod_gzip处理。我也不知道你说的“保存数据”是什么意思。这里的目标是减少服务器上的内存使用,允许它简单地将数据从一个地方流到另一个地方,而不需要一次保存太多数据。我担心的一件事是,当您使用StringIO时,会不会最终收集内存中的所有数据?已经从StringIO中读取的内容是否会被释放?您可能是对的,但是StringIO对于这个实现来说并不是必不可少的,只是最容易使用。您可以创建一个类似文件的对象,它总是只处理最后一个块。在连续的块中将数据写到zipfile应该很容易。如果ZipInfo对象位于归档文件的末尾(最新添加的),则它们应该只支持“写入”操作。但是当
这是hello{0}内容\n
的大小为10GB时,您会怎么做?
import zipfile
import sys


class StreamFile(object):
    def __init__(self, fileobj):
        self.fileobj = fileobj
        self.pos = 0

    def write(self, str):
        self.fileobj.write(str)
        self.pos += len(str)

    def tell(self):
        return self.pos

    def flush(self):
        self.fileobj.flush()


# Wrap a stream so ZipFile can use it
out = StreamFile(sys.stdout)
z = zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED)

for i in range(5):
    z.writestr("hello{0}.txt".format(i), "this is hello{0} contents\n".format(i) * 3)

z.close()
class ZipStreamer(object):
    def __init__(self):
        self.out_stream = StringIO.StringIO()

        # write to the stringIO with no compression
        self.zipfile = zipfile.ZipFile(self.out_stream, 'w', zipfile.ZIP_STORED)

        self.current_file = None

        self._last_streamed = 0

    def put_file(self, name, date_time=None):
        if date_time is None:
            date_time = time.localtime(time.time())[:6]

        zinfo = zipfile.ZipInfo(name, date_time)
        zinfo.compress_type = zipfile.ZIP_STORED
        zinfo.flag_bits = 0x08
        zinfo.external_attr = 0600 << 16
        zinfo.header_offset = self.out_stream.pos

        # write right values later
        zinfo.CRC = 0
        zinfo.file_size = 0
        zinfo.compress_size = 0

        self.zipfile._writecheck(zinfo)

        # write header to mega_streamer
        self.out_stream.write(zinfo.FileHeader())

        self.current_file = zinfo

    def flush(self):
        zinfo = self.current_file
        self.out_stream.write(
            struct.pack("<LLL", zinfo.CRC, zinfo.compress_size,
                        zinfo.file_size))
        self.zipfile.filelist.append(zinfo)
        self.zipfile.NameToInfo[zinfo.filename] = zinfo
        self.current_file = None

    def write(self, bytes):
        self.out_stream.write(bytes)
        self.out_stream.flush()
        zinfo = self.current_file
        # update these...
        zinfo.CRC = zipfile.crc32(bytes, zinfo.CRC) & 0xffffffff
        zinfo.file_size += len(bytes)
        zinfo.compress_size += len(bytes)

    def read(self):
        self.out_stream.seek(self._last_streamed)
        bytes = self.out_stream.read()
        self._last_streamed = 0

        # cleaning up memory in each iteration
        self.out_stream.seek(0) 
        self.out_stream.truncate()
        self.out_stream.flush()

        return bytes

    def close(self):
        self.zipfile.close()
def stream_generator(files_paths):
    s = ZipStreamer()
    for f in files_paths:
        s.put_file(f)
        with open(f) as _f:
            s.write(_f.read())
        s.flush()
        yield s.read()
    s.close()
class StreamZipEndpoint(object):
    def on_get(self, req, resp):
        files_pathes = [
            '/path/to/file/1',
            '/path/to/file/2',
        ]
        zip_filename = 'output_filename.zip'
        resp.content_type = 'application/zip'
        resp.set_headers([
            ('Content-Disposition', 'attachment; filename="%s"' % (
                zip_filename,))
        ])

        resp.stream = stream_generator(files_pathes)