使用cPickle序列化一个大字典导致了MemoryError

8

我正在为一组文档编写搜索引擎的倒排索引。目前,我将索引存储为字典的字典。也就是说,每个关键字映射到一个文档ID->出现位置列表的字典。

数据模型类似于: {word : { doc_name : [location_list] } }

在内存中构建索引很好用,但是当我尝试将其序列化到磁盘时,会遇到MemoryError错误。这是我的代码:

# Write the index out to disk
serializedIndex = open(sys.argv[3], 'wb')
cPickle.dump(index, serializedIndex, cPickle.HIGHEST_PROTOCOL)

在序列化之前,我的程序使用了大约50%的内存(1.6 Gb)。一旦我调用cPickle,我的内存使用率飙升到80%,然后崩溃。

为什么cPickle在序列化时使用了这么多内存?有没有更好的方法来解决这个问题?

3个回答

10

cPickle需要使用大量的额外内存,因为它需要进行循环检测。如果您确定数据没有循环引用,可以尝试使用marshal模块。


1
运行得非常好。修复非常简单--基本上只是将“pickle”更改为“marshal”,然后就完成了。我没有意识到cPickle执行循环检测。通过使用marshal,写入磁盘只需要几秒钟,而不是20分钟,并且将内存消耗从30%和崩溃降至几乎0%。谢谢! - Stephen Poletto
简单的解决方案加上简明扼要的解释,100%棒极了。 - mitchus
@John,我们如何知道数据没有循环? - João Almeida
@JoãoAlmeida,通常情况下,对象不包含对自身的引用(包括嵌套引用),您应该知道您的对象是否包含。一个简单的例子是包含循环的双向链表。 - John La Rooy

0

您可能在使用错误的工具进行此任务。如果您想要持久化大量索引数据,我强烈建议使用SQLite on-disk数据库(或者当然,只是普通数据库),并使用像SQLObjectSQLAlchemy这样的ORM。

这些将处理繁琐的事情,如兼容性,优化格式以用于目的,并且不会同时将所有数据保存在内存中,以便您不会耗尽内存...

补充:因为我正在处理几乎相同的事情,但主要是因为我是一个好人,所以这里有一个演示似乎可以做到您所需的事情(它将在当前目录中创建一个SQLite文件,如果具有该名称的文件已经存在,则删除它,请先将其放置在空位置):

import sqlobject
from sqlobject import SQLObject, UnicodeCol, ForeignKey, IntCol, SQLMultipleJoin
import os

DB_NAME = "mydb"
ENCODING = "utf8"

class Document(SQLObject):
    dbName = UnicodeCol(dbEncoding=ENCODING)

class Location(SQLObject):
    """ Location of each individual occurrence of a word within a document.
    """
    dbWord = UnicodeCol(dbEncoding=ENCODING)
    dbDocument = ForeignKey('Document')
    dbLocation = IntCol()

TEST_DATA = {
    'one' : {
        'doc1' : [1,2,10],
        'doc3' : [6],
    },

    'two' : {
        'doc1' : [2, 13],
        'doc2' : [5,6,7],
    },

    'three' : {
        'doc3' : [1],
    },
}        

if __name__ == "__main__":
    db_filename = os.path.abspath(DB_NAME)
    if os.path.exists(db_filename):
        os.unlink(db_filename)
    connection = sqlobject.connectionForURI("sqlite:%s" % (db_filename))
    sqlobject.sqlhub.processConnection = connection

    # Create the tables
    Document.createTable()
    Location.createTable()

    # Import the dict data:
    for word, locs in TEST_DATA.items():
        for doc, indices in locs.items():
            sql_doc = Document(dbName=doc)
            for index in indices:
                Location(dbWord=word, dbDocument=sql_doc, dbLocation=index)

    # Let's check out the data... where can we find 'two'?
    locs_for_two = Location.selectBy(dbWord = 'two')

    # Or...
    # locs_for_two = Location.select(Location.q.dbWord == 'two')

    print "Word 'two' found at..."
    for loc in locs_for_two:
        print "Found: %s, p%s" % (loc.dbDocument.dbName, loc.dbLocation)

    # What documents have 'one' in them?
    docs_with_one = Location.selectBy(dbWord = 'one').throughTo.dbDocument

    print
    print "Word 'one' found in documents..."
    for doc in docs_with_one:
        print "Found: %s" % doc.dbName

这绝不是唯一的方法(也不一定是最好的方法)来完成此操作。文档或单词表是否应该与位置表分开取决于您的数据和典型用法。在您的情况下,“Word”表可能可以作为一个单独的表,具有一些用于索引和唯一性的附加设置。


谢谢你的建议。目前,我打算使用marshal而不是pickle,但是将来我可能会重新考虑并迁移到基于数据库的解决方案。干杯! - Stephen Poletto
@Stephen Poletto - 很酷,如果marshal可以工作,那就没问题了,这个可以留在这里供后人参考 :) - detly

0

你可以尝试另一个pickle库。此外,您可能需要更改一些cPickle设置。

其他选项:将字典分成较小的部分,并对每个部分进行cPickle。然后在加载所有内容时将它们放回到一起。

抱歉这有点模糊,我只是随口写的。我想这可能仍然有帮助,因为没有其他人回答。


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