从文件中获取字数并为单词添加后缀 - Python

Getting word counts from a file and adding suffix to the word - Python

给定一个文本文件,例如:

This is a foo bar sentence .
And this is the first txtfile in the corpus .

目标是获得一个 Counter 对象,其中包含计数及其键作为每个单词字符的元组,即:

Counter({('i', 's', '</w>'): 2, ('t', 'h', 'e', '</w>'): 2, ('.', '</w>'): 2, ('T', 'h', 'i', 's', '</w>'): 1, ('f', 'i', 'r', 's', 't', '</w>'): 1, ('t', 'x', 't', 'f', 'i', 'l', 'e', '</w>'): 1, ('f', 'o', 'o', '</w>'): 1, ('t', 'h', 'i', 's', '</w>'): 1, ('s', 'e', 'n', 't', 'e', 'n', 'c', 'e', '</w>'): 1, ('A', 'n', 'd', '</w>'): 1, ('b', 'a', 'r', '</w>'): 1, ('c', 'o', 'r', 'p', 'u', 's', '</w>'): 1, ('a', '</w>'): 1, ('i', 'n', '</w>'): 1})

我尝试打开文件,(i) 按空格将文件读入单词列表,(ii) 然后使用 map lambda 将单词拆分为字符元组,然后添加 </w> 后缀,并 (iii) 将其转换为 Counter 对象:

$ echo -e """This is a foo bar sentence .\nAnd this is the first txtfile in the corpus .""" > test.txt
$ cat test.txt 
This is a foo bar sentence .
And this is the first txtfile in the corpus .
$ python
>>> from collections import Counter
>>> open('test.txt').read().split()
['This', 'is', 'a', 'foo', 'bar', 'sentence', '.', 'And', 'this', 'is', 'the', 'first', 'txtfile', 'in', 'the', 'corpus', '.']
>>> Counter(open('test.txt').read().split())
Counter({'is': 2, '.': 2, 'the': 2, 'a': 1, 'And': 1, 'bar': 1, 'sentence': 1, 'This': 1, 'txtfile': 1, 'this': 1, 'in': 1, 'foo': 1, 'corpus': 1, 'first': 1})
>>> Counter(map(lambda x: tuple(list(x)+['</w>']), open('test.txt').read().split()))
Counter({('i', 's', '</w>'): 2, ('t', 'h', 'e', '</w>'): 2, ('.', '</w>'): 2, ('T', 'h', 'i', 's', '</w>'): 1, ('f', 'i', 'r', 's', 't', '</w>'): 1, ('t', 'x', 't', 'f', 'i', 'l', 'e', '</w>'): 1, ('f', 'o', 'o', '</w>'): 1, ('t', 'h', 'i', 's', '</w>'): 1, ('s', 'e', 'n', 't', 'e', 'n', 'c', 'e', '</w>'): 1, ('A', 'n', 'd', '</w>'): 1, ('b', 'a', 'r', '</w>'): 1, ('c', 'o', 'r', 'p', 'u', 's', '</w>'): 1, ('a', '</w>'): 1, ('i', 'n', '</w>'): 1})

想象一下,如果文件真的很大,有 2,000,000 行,平均每行 40 个单词,每个单词 10 个字符。

给定文件大小,open('file.txt').read().split() 会不会效率低下?

如果我先逐行读取文件,然后逐字读入计数器,然后遍历计数器以添加后缀并将单词拆分为字符元组,这样会更好吗?

>>> x = Counter()
>>> for line in open('test.txt'):
...     for word in line.split():
...             x[word]+=1
... 
>>> x = Counter({tuple(list(k)+['</w>']):v for k,v in x.items()})
>>> x
Counter({('i', 's', '</w>'): 2, ('t', 'h', 'e', '</w>'): 2, ('.', '</w>'): 2, ('T', 'h', 'i', 's', '</w>'): 1, ('t', 'x', 't', 'f', 'i', 'l', 'e', '</w>'): 1, ('f', 'o', 'o', '</w>'): 1, ('t', 'h', 'i', 's', '</w>'): 1, ('s', 'e', 'n', 't', 'e', 'n', 'c', 'e', '</w>'): 1, ('f', 'i', 'r', 's', 't', '</w>'): 1, ('b', 'a', 'r', '</w>'): 1, ('c', 'o', 'r', 'p', 'u', 's', '</w>'): 1, ('a', '</w>'): 1, ('i', 'n', '</w>'): 1, ('A', 'n', 'd', '</w>'): 1})

是否有更好的方法来实现相同的Counter对象?


实现上面的Counter目的,就是让我根据词汇表中单词的连续字符对,得到一个二次Counter,即:

>>> from collections import Counter                                                 
>>> from itertools import tee
>>> v = Counter(map(lambda x: tuple(list(x)+['</w>']), open('test.txt').read().split()))
>>> def pairwise(i): a,b = tee(i); next(b,None); return zip(a,b)
... 
>>> Counter(*zip(*iter(pairwise(word) for word in v)))
Counter({('t', 'h'): 2, ('t', 'x'): 1, ('f', 'o'): 1, ('a', '</w>'): 1, ('A', 'n'): 1, ('b', 'a'): 1, ('s', 'e'): 1, ('T', 'h'): 1, ('.', '</w>'): 1, ('i', 's'): 1, ('c', 'o'): 1, ('f', 'i'): 1, ('i', 'n'): 1})

最后我需要两个 Counter 以便我可以使用 .most_common() 功能:

Counter({('i', 's', '</w>'): 2, ('t', 'h', 'e', '</w>'): 2, ('.', '</w>'): 2, ('T', 'h', 'i', 's', '</w>'): 1, ('f', 'i', 'r', 's', 't', '</w>'): 1, ('t', 'x', 't', 'f', 'i', 'l', 'e', '</w>'): 1, ('f', 'o', 'o', '</w>'): 1, ('t', 'h', 'i', 's', '</w>'): 1, ('s', 'e', 'n', 't', 'e', 'n', 'c', 'e', '</w>'): 1, ('A', 'n', 'd', '</w>'): 1, ('b', 'a', 'r', '</w>'): 1, ('c', 'o', 'r', 'p', 'u', 's', '</w>'): 1, ('a', '</w>'): 1, ('i', 'n', '</w>'): 1})

Counter({('t', 'h'): 2, ('t', 'x'): 1, ('f', 'o'): 1, ('a', '</w>'): 1, ('A', 'n'): 1, ('b', 'a'): 1, ('s', 'e'): 1, ('T', 'h'): 1, ('.', '</w>'): 1, ('i', 's'): 1, ('c', 'o'): 1, ('f', 'i'): 1, ('i', 'n'): 1})

是不是Counter(*zip(*iter(pairwise(word) for word in v)))也很低效?

计数器看起来可能只是一本字典。通过读取文件,您可以使用 readline() 逐行读取,而不是使用 read() 读取整个文件,然后必须再次迭代它才能完成工作。

下面将逐行读取文件中的行,如果需要,去除尾字符,然后将单词转换为您指定的字符元组。然后它添加或递增字典中的条目。

fp = 'some_filepath'
counter = {}
with open(fp) as fid:
    line = fid.readline()
    while line != '':
        line = line[:-1] if line[-1:] == '\n' else line
        words = [tuple(list(x) + ['</w>']) for x in line.split(' ')]
        for word in words:
            counter[word] = counter[word] + 1 if word in counter else 1
        line = fid.readline()
    fid.close()

是的,它效率低下 - 起初看起来是这样。

让我们先从纯粹的高层次角度来探讨这个问题。

read() 将整个文件读入内存(因为你实际上是从文件的内容构造一个字符串,需要为其分配内存)。

对于大文件,这是禁止的:

  • 假设每个单词都是恰好 10 个字符的字母数字字符串,这转换为(使用 sys.getsizeof())每个单词大约 47 个字节。
  • 假设每个句子有 40 个单词 - 这使我们每个句子最多有 1880 个字节。
  • 将其乘以 200 万,然后除以 10**9(因为 1 GB 等于字节数),我们发现这是 一个 3.76 GB 的文件

通过简单地存储该字符串,您将 运行 快速进入 32 位系统上的 space 限制,甚至可能 运行 对抗每个用户 space 操作系统设置的进程内存限制。大多数 64 位系统应该没问题,但绝对没有理由在您的系统上产生如此高的负载。在如此庞大的字符串上使用 split() 会进一步加重您的系统负担,因为这会在您构建字符串时 复制 您的记忆一段时间(因为您之前没有删除该字符串,虽然 Python 中的垃圾收集器通常会在 运行ning 程序中所有对它的引用都为零时为有问题的字符串释放内存。

但是,我们推断,逐行读取是一个 流式传输 过程 - 文件本身被视为一个迭代器对象,随后的每一行都是延迟生成的。您不是首先创建一个大对象并强制 Python 对其进行操作 - 而是您正在创建较小的对象并缓慢增加 space 分配。当您完成时,您的内存仍将达到 3.76 GB,但您可以通过慢慢地 优雅地 完成。

所以,是的,乍一看,看起来你犯了一个大错误,逐行阅读是更好的选择。

但这真的重要吗?

换句话说,我们对瓶颈的解释是否真的有效?

唯一的方法是测试。首先,一些CPU信息供大家参考:

Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                8
On-line CPU(s) list:   0-7
Thread(s) per core:    2
Core(s) per socket:    4
Socket(s):             1
NUMA node(s):          1
Vendor ID:             GenuineIntel
CPU family:            6
Model:                 42
Stepping:              7
CPU MHz:               800.000
BogoMIPS:              3990.96
Virtualization:        VT-x
L1d cache:             32K
L1i cache:             32K
L2 cache:              256K
L3 cache:              6144K
NUMA node0 CPU(s):     0-7

我有 8 GB 的 RAM。

我创建了一个大约 306 MB 的参考文件,包含大约 727,947 行,每行 40 个单词,长度为 10(所有字符都是随机生成的 - 创建一个 3 GB 的文件会花费太长时间):

import random
import string
with open("test.txt", "w+") as out:
    f = lambda x: ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(x))
    for i in xrange(727947):
        out.write(' '.join(f(10) for q in xrange(40)) + '\n')

读取和分配仅用了两秒钟:

akshat@Centaur:~$ time python2.7 -c 'with open("test.txt", "r") as f: f.read().split()'

real    0m1.768s
user    0m1.258s
sys     0m0.507s

(您可以使用htop查看此单独进程在其短暂存在期间使用的驻留内存)。

由于 1 GB 大约是 100 MB 的 10 倍,我们推断并认为读取如此大的文件大约需要 20 秒。

Counter(*zip(*iter(pairwise(word) for word in v)))怎么样?我在我的机器上对你程序的结果进行了计时:

from collections import Counter                                                 
from itertools import tee
v = Counter(map(lambda x: tuple(list(x)+['</w>']), open('test.txt').read().split()))
def pairwise(i): a,b = tee(i); next(b,None); return zip(a,b)
Counter(*zip(*iter(pairwise(word) for word in v)))

它导致我的系统挂起,迫使我在我的计算机变得无响应时尝试终止进程:

akshat@Centaur:~$ time python2.7 test.py
Traceback (most recent call last):
  File "test.py", line 4, in <module>
    v = Counter(map(lambda x: tuple(list(x)+['</w>']), open('test.txt').read().split()))


real    3m37.346s
user    1m2.196s
sys     0m4.050s

请注意,这还没有完成!这是该程序过早退出的结果 - 仅 map 并对其应用 Counter 就花了三分钟,它 still 未完成。请注意,在此间隔期间的任何给定时刻,我只有两个或三个其他 运行ning 进程 - 您可以在不太繁忙的机器上自由重现我的结果,但这一点仍然成立。

这是一个区区 300 MB 的文件,与您要处理的内容相去甚远。

让我们试试您的其他方法 - 逐行阅读并逐步更新:

x = Counter()

for line in open('test.txt'):
    for word in line.split():
        x[word]+=1

x = Counter({tuple(list(k)+['</w>']):v for k,v in x.items()})    

当然,这也导致我的系统崩溃,但它崩溃的那一行特别能说明问题:

akshat@Centaur:~$ time python2.7 test.py

Traceback (most recent call last):
  File "test.py", line 8, in <module>
    x = Counter({tuple(list(k)+['</w>']):v for k,v in x.items()})
  File "test.py", line 8, in <dictcomp>
    x = Counter({tuple(list(k)+['</w>']):v for k,v in x.items()})
KeyboardInterrupt

real    4m26.686s
user    2m7.868s
sys     0m3.017s

我们已经知道读取文件不是主要瓶颈,因为我们已经测试了假设的最坏情况下需要多长时间。

更有趣的是 Counter() 甚至还没有开始发挥作用。我在导致挂起的行中没有使用 Counter() 的情况下测试了这两个版本,发现仅应用 map() 或单独进行字典理解会导致我的系统挂起 - 都不会在不到五分钟内完成,这意味着他们自己要对我们看到的大量 运行 时间负责。

不,瓶颈 总是 当我们试图将某些内容映射到文件内容上时就会出现。 这是程序中最占用内存的部分,即使是中等大小的文件也需要几分钟才能完成。

(如果有人关心让这两个并排完成,我会很乐意在这里更新我的答案以反映这些数字)。

那么我们学到了什么?

  • 效率低下在于映射您的列表

  • 在使用 read() 或使用 readlines() 而不是 - 它们实际上占用了总时间的 0.66%(两秒)两张地图都超出了五分钟的上限)。即使我们假设增长与输入的大小呈线性关系,该比例也不会改变。

  • Premature optimization is the root of all evil.

你担心的完全是错误的事情。

您的瓶颈在于尝试将相当复杂的操作应用于内存中保存的相当庞大的列表,即使该列表据说比您要处理的范围小十倍。您在 Python 级别所做的任何事情都无法解决它。

就这么简单。修复

  • 要么尝试在每行数据中尽可能多地进行计算,然后逐步将它们相加以形成更大的对象,但不能保证这会有帮助,或者

  • 并行处理您的工作。为给定单词生成 count 的问题很容易落入 MapReduce 范式,并且在适当的集群上使用 Apache Spark 或类似工具将极大地帮助您提出一个参考映射.