Python中存储大文件的最快方法

14

最近问了一个问题,关于如何将大型Python对象保存到文件中。我之前遇到过将巨大的Python字典转换为字符串并通过write()写入文件时出现的问题。现在我正在使用pickle。虽然它可以工作,但文件非常大(> 5 GB)。我在这方面缺乏经验。我想知道在存储到内存之前是否更快或甚至可能对此pickle文件进行压缩。


4
这有点像说当你的汽车出故障时,你可以向父母请求搭车去你需要去的地方,然后乘坐公共交通工具回来。 - Karl Knechtel
将其存储到硬盘。我认为将>5 GB写入硬盘需要很长时间。 - puk
@Karl 哈哈,我也是这么想的,如果有什么问题,它会慢得多。 - puk
1
@puk 我认为你误解了重点。JBernardo建议使用外部工具压缩数据,并使用本地Python库读取压缩数据(通过一个在读取时解压缩的接口,使文件在其余代码看来似乎从未被压缩过)。逻辑上的奇怪之处在于,同样的库也同样能够使用相同类型的接口写入文件,正如我和phihag的答案所示。 - Karl Knechtel
@puk 公平地说,外部工具可能会有不同的优化。例如,可以通过pbzip2将输出进行管道传输,它可以使用多个核心进行压缩(与Python的实现不同)。由于实际磁盘传输很可能是瓶颈,通过pzip2(或另一个外部压缩器)进行管道传输并不能帮助太多。使用gzip或bzip2等标准压缩算法的美妙之处在于,您可以随时在外部工具和Python程序之间自由切换。 - phihag
5个回答

10

你可以使用bzip2压缩数据:


from __future__ import with_statement # Only for Python 2.5
import bz2,json,contextlib

hugeData = {'key': {'x': 1, 'y':2}}
with contextlib.closing(bz2.BZ2File('data.json.bz2', 'wb')) as f:
  json.dump(hugeData, f)

按照以下方式加载:

from __future__ import with_statement # Only for Python 2.5
import bz2,json,contextlib

with contextlib.closing(bz2.BZ2File('data.json.bz2', 'rb')) as f:
  hugeData = json.load(f)

您还可以使用zlibgzip对数据进行压缩,接口几乎相同。但是,请注意,无论是zlib还是gzip的压缩率都会低于通过bzip2(或lzma)实现的压缩率。


我不熟悉那个语法,但我理解BZ2File将其压缩到内存中,然后我可以通过JSON(或我所假设的Pickle)进行转储。 - puk
@puk 用什么语法?with语句只是一种语法糖,它确保文件被关闭。你可以将其理解为f = bz2.BZ2File(...)。这不会在内存中存储大量数据,这是一件好事。数据将会实时序列化和压缩,除了操作系统缓存之外,内存使用量不应该显著大于一个兆字节。 - phihag
1
@puk 不,with 对内存没有影响。正如我所说,它只确保文件被正确关闭。更多细节请参考文档 - phihag
1
这实际上是和我的方法一样,只是选择了不同的压缩方式,并且对代码编写方式更加明确(并展示了处理文件的常见用法)。@phihag,bz2.BZ2File是否已经实现了上下文管理接口,就像原生文件被重新改进一样?在这里真的需要使用contextlib.closing吗? - Karl Knechtel
1
@KarlKnechtel,这确实可以在Python 2.7+中实现。但我想要支持2.5+,因为在许多系统上,2.6(甚至2.5)仍然是标准的Python版本。 - phihag

5
Python代码在实现数据序列化时会非常慢。 如果您尝试在纯Python中创建与Pickle等效的内容,您会发现它非常缓慢。 幸运的是,内置模块执行这些操作非常出色。
除了cPickle外,您还会找到marshal模块,它速度更快。 但它需要一个真正的文件句柄(而不是来自类似文件的对象)。 您可以导入marshal作为Pickle并查看差异。 我不认为您可以创建比这更快的自定义序列化程序...
这里有一个实际的(不太旧的)Python序列化器的严肃基准测试

我觉得问这个问题很蠢,但为什么“序列化”很慢,而marshal比pickle快100倍?我能轻松地切换到marshal吗,还是需要进行大规模的改动? - puk
将数据结构转储到流中称为序列化。您可以轻松切换到cPickle / marshal(我以为您正在使用cPickle)。 - cJ Zougloub
不,我使用的是普通的pickle(ppickle?)。我刚刚进行了一个简单的测试,比较了marshal和pickle,哇,我们谈论的是两个数量级的差异。 - puk
马歇尔文档说:“如果您正在序列化和反序列化Python对象,请改用pickle模块-它的性能相当。”https://docs.python.org/2/library/marshal.html - compie

1
我只是想在phihag的回答上做出补充。
当尝试序列化一个接近内存大小的对象时,应该避免使用pickle/cPickle,因为它需要额外的1-2倍对象大小的内存来序列化。即使将其流式传输到BZ2File中也是如此。在我的情况下,我甚至已经用完了交换空间。
但JSON(以及与链接文章中提到的HDF文件类似)的问题在于它无法序列化元组,而在我的数据中,元组被用作字典的键。这没有很好的解决方案;我能找到的最好的方法是将元组转换为字符串,这需要一些自己的内存,但比pickle少得多。现在,您还可以使用ujson库,它比json库快得多。
对于由字符串组成的元组(要求字符串不包含逗号):
import ujson as json
from bz2 import BZ2File

bigdata = { ('a','b','c') : 25, ('d','e') : 13 }
bigdata = dict([(','.join(k), v) for k, v in bigdata.viewitems()]) 

f = BZ2File('filename.json.bz2',mode='wb')
json.dump(bigdata,f)
f.close()

重新组合元组:
bigdata = dict([(tuple(k.split(',')),v) for k,v in bigdata.viewitems()])

如果您的键是由两个整数组成的2元组,可以采用以下方式:

bigdata2 = { (1,2): 1.2, (2,3): 3.4}
bigdata2 = dict([('%d,%d' % k, v) for k, v in bigdata2.viewitems()])
# ... save, load ...
bigdata2 = dict([(tuple(map(int,k.split(','))),v) for k,v in bigdata2.viewitems()])

相比于 pickle,这种方法的另一个优点是,在使用 bzip2 压缩时,json 可以明显地更好地进行压缩。


1
当然可以,但是在写入之前尝试在内存中创建一个显式的压缩副本(可能不适合!)没有任何意义,因为你可以使用内置标准库功能自动将其在写入时进行压缩。;)
请参见http://docs.python.org/library/gzip.html。基本上,您可以使用特殊类型的流来创建。
gzip.GzipFile("output file name", "wb")

然后像使用普通的open(...) (或者file(...))方法创建文件一样使用它。


我认为这就是我要做的事情,基本上在写入时将其压缩。 - puk

-1
看看谷歌的ProtoBuffers。虽然它们不是为大文件设计的,比如音视频文件,但对于对象序列化这样的情况,它们表现良好,因为它们就是为此而设计的。实践证明,有一天您可能需要更新文件结构,而ProtoBuffers将处理它。此外,它们高度优化了压缩和速度。而且您不必局限于Python,Java和C++都得到了很好的支持。

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