我的第一个建议是不要将arrayOfTrigrams 完全保存在内存中,而是使用流式传输。您正在从某个地方阅读它,因此您可以控制阅读方式。 Python 的生成器在这里非常方便。假设您正在从文件中读取它:
def read_trigrams(fobj):
unique = {}
def make_unique(w):
w = w.strip("\"'`!?,.():-;{}").lower()
return unique.setdefault(w, w)
fobj.seek(0, 2)
total_size = fobj.tell()
fobj.seek(0, 0)
read = 0
prev_words = []
for idx, line in enumerate(fobj):
read += len(line)
words = prev_words
words.extend(filter(None, (make_unique(w) for w in line.split())))
if len(words) > 3:
for i in range(len(words) - 3):
yield tuple(words[i:i+3])
prev_words = words[-2:]
这里有两件事:
- 我们正在使用生成器,因此我们不是读取整个文件并返回一个三元组列表,而是一个接一个地返回三元组。这有点慢,但可以节省内存。
- 我们确保最终,我们读取的每个字符串最多只有一个副本,方法是自己拥有一个字符串字典。虽然一开始可能看起来很奇怪,但从文件
N 读取相同的字节序列S 时间确实占用N*len(S) 字节。通过使用字典,我们确保输入中的每个单词都有一个唯一的副本。当然,这确实会消耗一些内存。
这个函数对你来说可能看起来不同,这取决于你从哪里读取你的三元组。请记住,我在这里使用的分词器非常基础。
这已经节省了一点内存,不过不会太多。
所以,让我们将中间结果存储在磁盘上:
LIMIT = 5e6
def flush(counts, idx):
with open('counts-%d' % (idx,), 'wb') as fobj:
p = pickle.Pickler(fobj)
for item in sorted(counts.items()):
p.dump(item)
import sys
import pickle
from collections import defaultdict
counts = defaultdict(int)
caches = 0
with open(sys.argv[1], 'r') as fobj:
for t in read_trigrams(fobj):
counts[t] += 1
if len(counts) > LIMIT:
flush(counts, caches)
caches += 1
counts.clear()
flush(counts, caches)
在此步骤中,您可以调整 LIMIT 以不使用太多内存,即减少它直到您不再遇到 MemoryError。
现在,您的驱动器上有N 文件,其中包含已排序的三元组列表。在单独的程序中,您可以将它们读入并汇总所有中间计数:
import sys
import pickle
def merger(inputs):
unpicklers = [pickle.Unpickler(open(f, 'rb')) for f in inputs]
DONE = (object(), )
NEXT = (object(), )
peek = [NEXT] * len(unpicklers)
while True:
for idx in range(len(unpicklers)):
if peek[idx] is NEXT:
try:
peek[idx] = unpicklers[idx].load()
except EOFError:
peek[idx] = DONE
if all(v is DONE for v in peek):
return
min_key = min(v[0] for v in peek if v is not DONE)
yield min_key, sum(v[1] for v in peek if v[0] == min_key)
peek = [NEXT if (v[0] == min_key) else v for v in peek]
for trigram, count in merger(sys.argv[1:]):
print(trigram, count)
如果您有 4 GiB 的内存,您实际上可能必须使用拆分功能。使用 8 GiB,您应该能够将其全部保存在 RAM 中。