使用cPickle序列化大字典导致内存错误

8 投票
3 回答
2286 浏览
提问于 2025-04-16 12:03

我正在为一个搜索引擎编写一个倒排索引,处理一堆文档。目前,我把这个索引存储成一个字典的字典。也就是说,每个关键词都对应一个字典,这个字典里存储了文档ID和出现位置。

这个数据模型大概是这样的: {word : { doc_name : [location_list] } }

在内存中构建索引没问题,但当我尝试把它保存到硬盘时,出现了内存错误。我的代码是:

# 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 个回答

0

你可能用错了工具。如果你想保存大量的索引数据,我强烈建议你使用SQLite这种磁盘数据库(或者普通的数据库),再配合像SQLObjectSQL Alchemy这样的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

这当然不是唯一的方法(也不一定是最好的方法)来实现这个功能。至于“文档”表和“位置”表是否应该分开,这要看你的数据和使用习惯。在你的情况下,“词”表可能可以作为一个单独的表,并添加一些索引和唯一性的设置。

0

你可以试试另一个叫做pickle的库。另外,cPickle也有一些设置可以调整。

还有其他的选择:把你的字典分成小块,然后分别用cPickle保存每一块。等你需要用的时候再把它们组合起来。

抱歉我说得不太具体,我只是随便写的。不过我觉得这些信息可能还是有帮助的,因为没有其他人回答。

10

cPickle需要使用额外的内存,因为它会检查数据中是否有循环引用。如果你确定你的数据没有循环引用,可以试试marshal模块。

撰写回答