使用Python pickle加载大型字典

13 投票
5 回答
26212 浏览
提问于 2025-04-16 05:40

我有一个完整的倒排索引,它的形式是一个嵌套的Python字典。它的结构是:

{word : { doc_name : [location_list] } }

比如说,这个字典叫做index,那么对于一个词“spam”,它的条目看起来像这样:

{ spam : { doc1.txt : [102,300,399], doc5.txt : [200,587] } }

我使用这个结构是因为Python字典的性能很好,编程起来也更简单。

对于任何一个词“spam”,包含它的文档可以通过以下方式获取:

index['spam'].keys()

而文档doc1的发布列表可以通过:

index['spam']['doc1']

目前我使用cPickle来存储和加载这个字典。但是,生成的文件大约有380MB,加载时需要很长时间——大约112秒(我用time.time()计时的),而且内存使用量达到了1.2GB(Gnome系统监视器显示)。一旦加载完成,就没问题了。我有4GB的内存。

len(index.keys())的结果是229758

代码

import cPickle as pickle

f = open('full_index','rb')
print 'Loading index... please wait...'
index = pickle.load(f)  # This takes ages
print 'Index loaded. You may now proceed to search'

我该如何让它加载得更快呢?我只需要在应用程序启动时加载一次。之后,访问时间对响应查询很重要。

我是否应该换成像SQLite这样的数据库,并在它的键上创建索引?如果是的话,我该如何存储值,以便有一个等效的结构,方便检索?还有其他我应该关注的地方吗?

附录

使用Tim的回答pickle.dump(index, file, -1)后,生成的文件明显小了很多——大约237MB(花了300秒来生成)……现在加载的时间也减少了一半(61秒……相比之前的112秒……time.time()

但我是否应该迁移到数据库以便于扩展?

目前我将Tim的回答标记为接受。

PS:我不想使用Lucene或Xapian……这个问题参考了存储倒排索引。我不得不问一个新问题,因为我无法删除之前的问题。

5 个回答

3

在Python 2.x中,一个常见的做法是有一个用纯Python写的模块版本,还有一个可选的加速版本是用C语言扩展实现的;比如说,picklecPickle。这样一来,使用这些模块的每个人都需要自己去导入加速版本,如果导入失败,就会退回到纯Python版本。在Python 3.0中,加速版本被视为纯Python版本的实现细节。用户应该始终导入标准版本,这个版本会尝试导入加速版本,如果失败就会使用纯Python版本。 pickle和cPickle就是这样处理的。

  • 协议版本0是最初的“人类可读”协议,并且与早期版本的Python兼容。
  • 协议版本1是一个旧的二进制格式,也与早期版本的Python兼容。
  • 协议版本2是在Python 2.3中引入的。它能更高效地处理新式类的序列化。有关协议2带来的改进,请参考PEP 307。
  • 协议版本3是在Python 3.0中添加的。它明确支持字节对象,并且不能被Python 2.x反序列化。这是默认的协议,也是当需要与其他Python 3版本兼容时推荐使用的协议。
  • 协议版本4是在Python 3.4中添加的。它支持非常大的对象,可以序列化更多种类的对象,并且进行了一些数据格式的优化。有关协议4带来的改进,请参考PEP 3154

如果你的字典非常大,并且只需要与Python 3.4或更高版本兼容,可以使用:

pickle.dump(obj, file, protocol=4)
pickle.load(file, encoding="bytes")

或者:

Pickler(file, 4).dump(obj)
Unpickler(file).load()

话说回来,在2010年json模块在编码简单类型时比pickle快25倍,在解码时快15倍。我在2014年的基准测试显示marshal > pickle > json,但marshal与特定的Python版本相关联

7

你真的需要一次性加载所有内容吗?如果你并不需要把所有东西都放在内存里,而只是想在某个时刻选择性地使用部分数据,那么你可以考虑把你的字典映射到一组磁盘文件,而不是一个单独的文件……或者把字典映射到一个数据库表中。所以,如果你在寻找一种可以把大字典数据保存到磁盘或数据库的方法,并且能够使用序列化和编码(比如编码器和哈希表),那么你可以看看 klepto

klepto 提供了一种字典抽象,可以用来写入数据库,包括把你的文件系统当作数据库来使用(也就是说,可以把整个字典写入一个文件,或者把每个条目写入自己的文件)。对于大数据,我通常选择把字典表示为文件系统中的一个目录,每个条目都是一个文件。klepto 还提供了缓存算法,所以如果你使用文件系统作为字典的后端,你可以通过使用内存缓存来避免一些速度上的损失。

>>> from klepto.archives import dir_archive
>>> d = {'a':1, 'b':2, 'c':map, 'd':None}
>>> # map a dict to a filesystem directory
>>> demo = dir_archive('demo', d, serialized=True) 
>>> demo['a']
1
>>> demo['c']
<built-in function map>
>>> demo          
dir_archive('demo', {'a': 1, 'c': <built-in function map>, 'b': 2, 'd': None}, cached=True)
>>> # is set to cache to memory, so use 'dump' to dump to the filesystem 
>>> demo.dump()
>>> del demo
>>> 
>>> demo = dir_archive('demo', {}, serialized=True)
>>> demo
dir_archive('demo', {}, cached=True)
>>> # demo is empty, load from disk
>>> demo.load()
>>> demo
dir_archive('demo', {'a': 1, 'c': <built-in function map>, 'b': 2, 'd': None}, cached=True)
>>> demo['c']
<built-in function map>
>>> 

klepto 还有其他选项,比如 compressionmemmode,可以用来定制你的数据存储方式(例如,压缩级别、内存映射模式等)。使用数据库(比如 MySQL 等)作为后端也同样简单(接口完全相同),你也可以关闭内存缓存,这样每次读写都会直接到归档中,只需设置 cached=False

klepto 还允许你自定义编码,通过构建一个自定义的 keymap

>>> from klepto.keymaps import *
>>> 
>>> s = stringmap(encoding='hex_codec')
>>> x = [1,2,'3',min]
>>> s(x)
'285b312c20322c202733272c203c6275696c742d696e2066756e6374696f6e206d696e3e5d2c29'
>>> p = picklemap(serializer='dill')
>>> p(x)
'\x80\x02]q\x00(K\x01K\x02U\x013q\x01c__builtin__\nmin\nq\x02e\x85q\x03.'
>>> sp = s+p
>>> sp(x)
'\x80\x02UT28285b312c20322c202733272c203c6275696c742d696e2066756e6374696f6e206d696e3e5d2c292c29q\x00.' 

klepto 还提供了很多缓存算法(比如 mrulrulfu 等),帮助你管理内存中的缓存,并会使用这些算法来处理归档后端的存取。

你可以使用 cached=False 来完全关闭内存缓存,直接从磁盘或数据库读写。如果你的条目足够大,你可以选择写入磁盘,把每个条目放在自己的文件里。这里有一个同时做这两件事的例子。

>>> from klepto.archives import dir_archive
>>> # does not hold entries in memory, each entry will be stored on disk
>>> demo = dir_archive('demo', {}, serialized=True, cached=False)
>>> demo['a'] = 10
>>> demo['b'] = 20
>>> demo['c'] = min
>>> demo['d'] = [1,2,3]

不过,虽然这样应该能大大减少加载时间,但可能会稍微降低整体执行速度……通常最好是指定一个最大值来保持在内存缓存中的数据量,并选择一个好的缓存算法。你需要多尝试一下,以找到适合你需求的平衡。

在这里获取 kleptohttps://github.com/uqfoundation

14

在使用 cPickle.dumpcPickle.dumps 时,可以试试协议参数。根据 cPickle.Pickler.__doc__ 的说明:

Pickler(file, protocol=0) -- 创建一个序列化工具。

这个工具需要一个像文件一样的对象,用来写入序列化的数据流。可选的 proto 参数告诉这个工具使用指定的协议;支持的协议有 0、1、2。默认的协议是 0,这样可以保证向后兼容。(协议 0 是唯一一个可以在文本模式下打开的文件中写入并成功读取的协议。当使用高于 0 的协议时,确保文件以二进制模式打开,无论是序列化还是反序列化时。)

协议 1 比协议 0 更高效;协议 2 比协议 1 更高效。

如果指定一个负的协议版本,会选择支持的最高协议版本。使用的协议越高,读取生成的序列化数据所需的 Python 版本就越新。

文件参数必须有一个 write() 方法,并且这个方法接受一个字符串参数。因此,它可以是一个打开的文件对象、一个 StringIO 对象,或者任何其他符合这个接口的自定义对象。

将 JSON 或 YAML 转换成序列化格式通常会比序列化大多数数据花费更长的时间,因为 pickle 存储的是原生的 Python 类型。

撰写回答