Python - 迭代字典键时的性能

Python - Performance when iterating over dictionary keys

我有一个相对较大的文本文件(大约 700 万行),我想 运行 它的特定逻辑,我将在下面尝试解释:

A1KEY1
A2KEY1
B1KEY2
C1KEY3
D1KEY3
E1KEY4

我想统计按键出现的频率,然后将频率为1的输出到一个文本文件,频率为2的输出到另一个文本文件,频率大于2的输出到另一个文本文件.

这是我目前的代码,但它遍历字典的速度非常慢,而且越进展越慢。

def filetoliststrip(file):
    file_in = str(file)
    lines = list(open(file_in, 'r'))
    content = [x.strip() for x in lines] 
    return content


dict_in = dict()    
seen = []


fileinlist = filetoliststrip(file_in)
out_file = open(file_ot, 'w')
out_file2 = open(file_ot2, 'w')
out_file3 = open(file_ot3, 'w')

counter = 0

for line in fileinlist:
    counter += 1
    keyf = line[10:69]
    print("Loading line " + str(counter) + " : " + str(line))
if keyf not in dict_in.keys():
    dict_in[keyf] = []
    dict_in[keyf].append(1)
    dict_in[keyf].append(line)
else:
    dict_in[keyf][0] += 1
    dict_in[keyf].append(line)


for j in dict_in.keys():
    print("Processing key: " + str(j))
    #print(dict_in[j])
    if dict_in[j][0] < 2:
        out_file.write(str(dict_in[j][1]))
    elif dict_in[j][0] == 2:
        for line_in in dict_in[j][1:]:
            out_file2.write(str(line_in) + "\n")
    elif dict_in[j][0] > 2:
        for line_in in dict_in[j][1:]:
            out_file3.write(str(line_in) + "\n")


out_file.close()
out_file2.close()
out_file3.close()

我运行在具有 8GB Ram 的 windows PC i7 上执行此操作,执行此操作应该不会花费数小时。这是我将文件读入列表的方式的问题吗?我应该使用不同的方法吗?提前致谢。

您一次将一个非常大的文件加载到内存中。当你实际上不需要这些线,而你只需要处理它时,使用 generator。它更节省内存。

Counter 是一个集合,其中元素存储为字典键,它们的计数存储为字典值。您可以使用它来计算按键的频率。然后简单地遍历新的 dict 并将密钥附加到相关文件:

from collections import Counter

keys = ['A1KEY1', 'A2KEY1', 'B1KEY2', 'C1KEY3', 'D1KEY3', 'E1KEY4']
count = Counter(keys)


with open('single.txt') as f1:
    with open('double.txt') as f2:
        with open('more_than_double.txt') as f3:

        for k, v in count.items():
            if v == 1:
                f1.writelines(k)
            elif v == 2:
                f2.writelines(k)
            else:
                f3.writelines(k)

您有多个点会减慢您的代码速度 - 无需将整个文件加载到内存中只是为了再次对其进行迭代,无需每次都获取键列表查找(if key not in dict_in: ... 就足够了,而且速度非常快),您不需要保留行数,因为您可以 post-检查行的长度...仅举几例。

我会将您的代码完全重构为:

import collections

dict_in = collections.defaultdict(list)  # save some time with a dictionary factory
with open(file_in, "r") as f:  # open the file_in for reading
    for line in file_in:  # read the file line by line
        key = line.strip()[10:69]  # assuming this is how you get your key
        dict_in[key].append(line)  # add the line as an element of the found key
# now that we have the lines in their own key brackets, lets write them based on frequency
with open(file_ot, "w") as f1, open(file_ot2, "w") as f2, open(file_ot3, "w") as f3:
    selector = {1: f1, 2: f2}  # make our life easier with a quick length-based lookup
    for values in dict_in.values():  # use dict_in.itervalues() on Python 2.x
        selector.get(len(values), f3).writelines(values)  # write the collected lines

而且您几乎不会比这更有效率,至少在 Python。

请记住,这不能保证 Python 3.7(或 CPython 3.6)之前输出中行的顺序。但是,键本身内的顺序将被保留。如果您需要保持上述 Python 版本之前的行顺序,您必须保留一个单独的键顺序列表并遍历它以按顺序获取 dict_in 值。

第一个函数:

def filetoliststrip(file):
    file_in = str(file)
    lines = list(open(file_in, 'r'))
    content = [x.strip() for x in lines] 
    return content

此处生成了一个原始行列表,仅供删除。这将需要大约两倍于必要的内存,而且同样重要的是,多次传递不适合缓存的数据。我们也不需要重复制作 str 的东西。所以我们可以稍微简化一下:

def filetoliststrip(filename):
    return [line.strip() for line in open(filename, 'r')]

这仍然会生成一个列表。如果我们只读取一次数据,而不是存储每一行​​,请将 [] 替换为 () 以将其转换为生成器表达式;在这种情况下,由于行实际上在程序结束之前都完整地保留在内存中,因此我们只会为列表保存 space (在您的情况下仍然至少有 30MB)。

然后我们有主要的解析循环(我调整了我认为应该的缩进):

counter = 0

for line in fileinlist:
    counter += 1
    keyf = line[10:69]
    print("Loading line " + str(counter) + " : " + str(line))
    if keyf not in dict_in.keys():
        dict_in[keyf] = []
        dict_in[keyf].append(1)
        dict_in[keyf].append(line)
    else:
        dict_in[keyf][0] += 1
        dict_in[keyf].append(line)

这里有几个次优的东西。

首先,计数器可以是 enumerate(如果没有可迭代对象,则有 rangeitertools.count)。更改此设置将有助于清晰度并减少出错的风险。

for counter, line in enumerate(fileinlist, 1):

其次,在一个操作中形成一个字符串比从位添加它更有效:

    print("Loading line {} : {}".format(counter, line))

第三,字典成员检查不需要提取键。在 Python 2 中构建一个新列表,这意味着复制键中保存的所有引用,并且每次迭代都会变慢。在Python 3中,仍然意味着不必要地构建一个关键视图对象。如果需要检查,只需使用 keyf not in dict_in

第四,支票确实不需要。在查找失败时捕获异常几乎与 if 检查一样快,并且在 if 检查之后重复查找几乎肯定更慢。就此而言,一般停止重复查找:

    try:
        dictvalue = dict_in[keyf]
        dictvalue[0] += 1
        dictvalue.append(line)
    except KeyError:
        dict_in[keyf] = [1, line]

这是一个如此常见的模式,但是,我们有 两个 标准库实现:Counterdefaultdict。我们可以在这里同时使用两者,但是当您只需要计数时,Counter 更实用。

from collections import defaultdict
def newentry():
    return [0]
dict_in = defaultdict(newentry)

for counter, line in enumerate(fileinlist, 1):
    keyf = line[10:69]
    print("Loading line {} : {}".format(counter, line))
    dictvalue = dict_in[keyf]
    dictvalue[0] += 1
    dictvalue.append(line)

使用defaultdict让我们不用担心条目是否存在。

我们现在到了输出阶段。同样,我们有不必要的查找,所以让我们将它们减少到一次迭代:

for key, value in dict_in.iteritems():  # just items() in Python 3
    print("Processing key: " + key)
    #print(value)
    count, lines = value[0], value[1:]
    if count < 2:
        out_file.write(lines[0])
    elif count == 2:
        for line_in in lines:
            out_file2.write(line_in + "\n")
    elif count > 2:
        for line_in in lines:
            out_file3.write(line_in + "\n")

还有一些烦恼。我们重复了编写代码,它构建了其他字符串(在 "\n" 上标记),并且它针对每种情况都有一大堆类似的代码。事实上,重复可能导致了一个错误:out_file 中的单次出现没有换行符。让我们找出真正不同的地方:

for key, value in dict_in.iteritems():  # just items() in Python 3
    print("Processing key: " + key)
    #print(value)
    count, lines = value[0], value[1:]
    if count < 2:
        key_outf = out_file
    elif count == 2:
        key_outf = out_file2
    else:  #  elif count > 2:  # Test not needed
        key_outf = out_file3
    key_outf.writelines(line_in + "\n" for line_in in lines)

我离开了换行连接,因为将它们混合为单独的调用更加复杂。该字符串是短暂的,它用于将换行符放在同一位置的目的:它使得在 OS 级别不太可能一行被并发写入打断。

您会注意到这里有 Python 2 和 3 个不同之处。如果 Python 3 中的 运行 首先,您的代码很可能并没有那么慢。存在一个名为 six 的兼容性模块,可以编写更容易 运行 的代码;它可以让你使用例如six.viewkeyssix.iteritems 来避免这个陷阱。