37

I'd like to count frequencies of all words in a text file.

>>> countInFile('test.txt')

should return {'aaa':1, 'bbb': 2, 'ccc':1} if the target text file is like:

# test.txt
aaa bbb ccc
bbb

I've implemented it with pure python following some posts. However, I've found out pure-python ways are insufficient due to huge file size (> 1GB).

I think borrowing sklearn's power is a candidate.

If you let CountVectorizer count frequencies for each line, I guess you will get word frequencies by summing up each column. But, it sounds a bit indirect way.

What is the most efficient and straightforward way to count words in a file with python?

Update

My (very slow) code is here:

from collections import Counter

def get_term_frequency_in_file(source_file_path):
    wordcount = {}
    with open(source_file_path) as f:
        for line in f:
            line = line.lower().translate(None, string.punctuation)
            this_wordcount = Counter(line.split())
            wordcount = add_merge_two_dict(wordcount, this_wordcount)
    return wordcount

def add_merge_two_dict(x, y):
    return { k: x.get(k, 0) + y.get(k, 0) for k in set(x) | set(y) }
4

8 回答 8

49

最简洁的方法是使用 Python 提供的工具。

from future_builtins import map  # Only on Python 2

from collections import Counter
from itertools import chain

def countInFile(filename):
    with open(filename) as f:
        return Counter(chain.from_iterable(map(str.split, f)))

而已。正在制作一个从每行map(str.split, f)返回 s 个单词的生成器。list包装chain.from_iterable将其转换为一次生成一个单词的单个生成器。Counter接受一个可迭代的输入并计算其中的所有唯一值。最后,您return是一个类似 dict对象 (a Counter),它存储所有唯一单词及其计数,并且在创建期间,您一次只存储一行数据和总计数,而不是一次存储整个文件。

理论上,在 Python 2.7 和 3.1 上,您可能会更好地自己循环链接结果并使用dictorcollections.defaultdict(int)来计数(因为Counter在 Python 中实现,这在某些情况下会使其变慢),但让Counter工作更简单和更多的自我记录(我的意思是,整个目标是计数,所以使用 a Counter)。除此之外,在 CPython(参考解释器)3.2 及更高版本上,Counter有一个 C 级加速器,用于计算可迭代输入,其运行速度比您用纯 Python 编写的任何东西都要快。

更新:您似乎希望去除标点符号和不区分大小写,所以这是我早期代码的一个变体:

from string import punctuation

def countInFile(filename):
    with open(filename) as f:
        linewords = (line.translate(None, punctuation).lower().split() for line in f)
        return Counter(chain.from_iterable(linewords))

您的代码运行速度要慢得多,因为它正在创建和销毁许多小Counter对象set,而不是 -每行.update一次Counter(虽然比我在更新的代码块中给出的稍慢,但至少在算法上在比例因子上相似)。

于 2016-03-08T02:30:21.447 回答
15

一种有效且准确的记忆方法是利用

  • CountVectorizer in scikit(用于 ngram 提取)
  • NLTK 为word_tokenize
  • numpy矩阵总和以收集计数
  • collections.Counter用于收集计数和词汇

一个例子:

import urllib.request
from collections import Counter

import numpy as np 

from nltk import word_tokenize
from sklearn.feature_extraction.text import CountVectorizer

# Our sample textfile.
url = 'https://raw.githubusercontent.com/Simdiva/DSL-Task/master/data/DSLCC-v2.0/test/test.txt'
response = urllib.request.urlopen(url)
data = response.read().decode('utf8')


# Note that `ngram_range=(1, 1)` means we want to extract Unigrams, i.e. tokens.
ngram_vectorizer = CountVectorizer(analyzer='word', tokenizer=word_tokenize, ngram_range=(1, 1), min_df=1)
# X matrix where the row represents sentences and column is our one-hot vector for each token in our vocabulary
X = ngram_vectorizer.fit_transform(data.split('\n'))

# Vocabulary
vocab = list(ngram_vectorizer.get_feature_names())

# Column-wise sum of the X matrix.
# It's some crazy numpy syntax that looks horribly unpythonic
# For details, see http://stackoverflow.com/questions/3337301/numpy-matrix-to-array
# and http://stackoverflow.com/questions/13567345/how-to-calculate-the-sum-of-all-columns-of-a-2d-numpy-array-efficiently
counts = X.sum(axis=0).A1

freq_distribution = Counter(dict(zip(vocab, counts)))
print (freq_distribution.most_common(10))

[出去]:

[(',', 32000),
 ('.', 17783),
 ('de', 11225),
 ('a', 7197),
 ('que', 5710),
 ('la', 4732),
 ('je', 4304),
 ('se', 4013),
 ('на', 3978),
 ('na', 3834)]

本质上,您也可以这样做:

from collections import Counter
import numpy as np 
from nltk import word_tokenize
from sklearn.feature_extraction.text import CountVectorizer

def freq_dist(data):
    """
    :param data: A string with sentences separated by '\n'
    :type data: str
    """
    ngram_vectorizer = CountVectorizer(analyzer='word', tokenizer=word_tokenize, ngram_range=(1, 1), min_df=1)
    X = ngram_vectorizer.fit_transform(data.split('\n'))
    vocab = list(ngram_vectorizer.get_feature_names())
    counts = X.sum(axis=0).A1
    return Counter(dict(zip(vocab, counts)))

让我们timeit

import time

start = time.time()
word_distribution = freq_dist(data)
print (time.time() - start)

[出去]:

5.257147789001465

请注意,CountVectorizer也可以使用文件而不是字符串,并且无需将整个文件读入内存。在代码中:

import io
from collections import Counter

import numpy as np
from sklearn.feature_extraction.text import CountVectorizer

infile = '/path/to/input.txt'

ngram_vectorizer = CountVectorizer(analyzer='word', ngram_range=(1, 1), min_df=1)

with io.open(infile, 'r', encoding='utf8') as fin:
    X = ngram_vectorizer.fit_transform(fin)
    vocab = ngram_vectorizer.get_feature_names()
    counts = X.sum(axis=0).A1
    freq_distribution = Counter(dict(zip(vocab, counts)))
    print (freq_distribution.most_common(10))
于 2016-03-10T12:53:51.623 回答
4

这是一些基准。它看起来很奇怪,但最粗略的代码会胜出。

[代码]:

from collections import Counter, defaultdict
import io, time

import numpy as np
from sklearn.feature_extraction.text import CountVectorizer

infile = '/path/to/file'

def extract_dictionary_sklearn(file_path):
    with io.open(file_path, 'r', encoding='utf8') as fin:
        ngram_vectorizer = CountVectorizer(analyzer='word')
        X = ngram_vectorizer.fit_transform(fin)
        vocab = ngram_vectorizer.get_feature_names()
        counts = X.sum(axis=0).A1
    return Counter(dict(zip(vocab, counts)))

def extract_dictionary_native(file_path):
    dictionary = Counter()
    with io.open(file_path, 'r', encoding='utf8') as fin:
        for line in fin:
            dictionary.update(line.split())
    return dictionary

def extract_dictionary_paddle(file_path):
    dictionary = defaultdict(int)
    with io.open(file_path, 'r', encoding='utf8') as fin:
        for line in fin:
            for words in line.split():
                dictionary[word] +=1
    return dictionary

start = time.time()
extract_dictionary_sklearn(infile)
print time.time() - start

start = time.time()
extract_dictionary_native(infile)
print time.time() - start

start = time.time()
extract_dictionary_paddle(infile)
print time.time() - start

[出去]:

38.306814909
24.8241138458
12.1182529926

上述基准测试中使用的数据大小(154MB):

$ wc -c /path/to/file
161680851

$ wc -l /path/to/file
2176141

需要注意的一些事项:

  • 使用该sklearn版本,矢量化器创建 + numpy 操作和转换为Counter对象的开销
  • 然后nativeCounter更新版本,好像Counter.update()是一个昂贵的操作
于 2016-10-06T08:59:16.730 回答
3

这应该足够了。

def countinfile(filename):
    d = {}
    with open(filename, "r") as fin:
        for line in fin:
            words = line.strip().split()
            for word in words:
                try:
                    d[word] += 1
                except KeyError:
                    d[word] = 1
    return d
于 2016-03-08T02:14:34.043 回答
2

我没有解码从 url 读取的整个字节,而是处理二进制数据。因为bytes.translate期望它的第二个参数是一个字节字符串,所以我 utf-8 编码punctuation。删除标点符号后,我对字节字符串进行 utf-8 解码。

该函数freq_dist需要一个可迭代的。这就是我通过的原因data.splitlines()

from urllib2 import urlopen
from collections import Counter
from string import punctuation
from time import time
import sys
from pprint import pprint

url = 'https://raw.githubusercontent.com/Simdiva/DSL-Task/master/data/DSLCC-v2.0/test/test.txt'

data = urlopen(url).read()

def freq_dist(data):
    """
    :param data: file-like object opened in binary mode or
                 sequence of byte strings separated by '\n'
    :type data: an iterable sequence
    """
    #For readability   
    #return Counter(word for line in data
    #    for word in line.translate(
    #    None,bytes(punctuation.encode('utf-8'))).decode('utf-8').split())

    punc = punctuation.encode('utf-8')
    words = (word for line in data for word in line.translate(None, punc).decode('utf-8').split())
    return Counter(words)


start = time()
word_dist = freq_dist(data.splitlines())
print('elapsed: {}'.format(time() - start))
pprint(word_dist.most_common(10))

输出;

elapsed: 0.806480884552

[(u'de', 11106),
 (u'a', 6742),
 (u'que', 5701),
 (u'la', 4319),
 (u'je', 4260),
 (u'se', 3938),
 (u'\u043d\u0430', 3929),
 (u'na', 3623),
 (u'da', 3534),
 (u'i', 3487)]

它似乎dictCounter对象更有效。

def freq_dist(data):
    """
    :param data: A string with sentences separated by '\n'
    :type data: str
    """
    d = {}
    punc = punctuation.encode('utf-8')
    words = (word for line in data for word in line.translate(None, punc).decode('utf-8').split())
    for word in words:
        d[word] = d.get(word, 0) + 1
    return d

start = time()
word_dist = freq_dist(data.splitlines())
print('elapsed: {}'.format(time() - start))
pprint(sorted(word_dist.items(), key=lambda x: (x[1], x[0]), reverse=True)[:10])

输出;

elapsed: 0.642680168152

[(u'de', 11106),
 (u'a', 6742),
 (u'que', 5701),
 (u'la', 4319),
 (u'je', 4260),
 (u'se', 3938),
 (u'\u043d\u0430', 3929),
 (u'na', 3623),
 (u'da', 3534),
 (u'i', 3487)]

为了在打开大文件时提高内存效率,您必须只传递打开的 url。但时间也将包括文件下载时间。

data = urlopen(url)
word_dist = freq_dist(data)
于 2016-03-11T15:45:00.993 回答
0

跳过 CountVectorizer 和 scikit-learn。

该文件可能太大而无法加载到内存中,但我怀疑 python 字典太大。对您来说最简单的选择可能是将大文件拆分为 10-20 个较小的文件,然后扩展您的代码以遍历较小的文件。

于 2016-03-08T02:10:43.670 回答
0

结合其他人的观点和我自己的一些观点:)这是我为你准备的

from collections import Counter
from nltk.tokenize import RegexpTokenizer
from nltk.corpus import stopwords

text='''Note that if you use RegexpTokenizer option, you lose 
natural language features special to word_tokenize 
like splitting apart contractions. You can naively 
split on the regex \w+ without any need for the NLTK.
'''

# tokenize
raw = ' '.join(word_tokenize(text.lower()))

tokenizer = RegexpTokenizer(r'[A-Za-z]{2,}')
words = tokenizer.tokenize(raw)

# remove stopwords
stop_words = set(stopwords.words('english'))
words = [word for word in words if word not in stop_words]

# count word frequency, sort and return just 20
counter = Counter()
counter.update(words)
most_common = counter.most_common(20)
most_common

输出

(全部)

[('注', 1),
 ('使用', 1),
 ('regexptokenizer', 1),
 ('选项1),
 ('输', 1),
 ('自然', 1),
 ('语言', 1),
 ('特征', 1),
 ('特殊', 1),
 ('字', 1),
 ('tokenize', 1),
 ('喜欢', 1),
 ('分裂', 1),
 ('分开', 1),
 (“宫缩”,1),
 ('天真', 1),
 ('分裂', 1),
 ('正则表达式', 1),
 ('没有', 1),
 ('需要', 1)]

在效率方面可以做得比这更好,但如果你不太担心,这段代码是最好的。

于 2020-01-26T08:22:36.240 回答
0

你可以试试sklearn

from sklearn.feature_extraction.text import CountVectorizer
    vectorizer = CountVectorizer()

    data=['i am student','the student suffers a lot']
    transformed_data =vectorizer.fit_transform(data)
    vocab= {a: b for a, b in zip(vectorizer.get_feature_names(), np.ravel(transformed_data.sum(axis=0)))}
    print (vocab)
于 2019-03-01T07:05:04.723 回答