J'aimerais compter les fréquences de tous les mots dans un fichier texte.
>>> countInFile('test.txt')
devrait renvoyer {'aaa':1, 'bbb': 2, 'ccc':1}
si le fichier texte cible ressemble à ceci:
# test.txt
aaa bbb ccc
bbb
Je l'ai implémenté avec du python pur après quelques posts . Cependant, j'ai découvert que les méthodes en python pur étaient insuffisantes en raison de la taille énorme du fichier (> 1 Go).
Je pense que le pouvoir d'emprunter Sklearn est un candidat.
Si vous laissez CountVectorizer compter les fréquences pour chaque ligne, je suppose que vous obtiendrez les fréquences Word en additionnant chaque colonne. Mais cela semble un peu indirect.
Quel est le moyen le plus efficace et le plus simple de compter les mots dans un fichier avec Python?
Mon code (très lent) est ici:
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) }
L’approche la plus succincte consiste à utiliser les outils que Python vous fournit.
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)))
C'est tout. map(str.split, f)
crée un générateur qui renvoie list
s de mots de chaque ligne. L'emballage dans chain.from_iterable
le convertit en un seul générateur qui produit un mot à la fois. Counter
prend une entrée itérable et compte toutes ses valeurs uniques. À la fin, vous return
un objet ressemblant à dict
(un Counter
) qui stocke tous les mots uniques et leur nombre, et lors de la création, vous ne stockez qu'une ligne de données à la fois et le nombre total, et non le fichier entier à la fois.
En théorie, sur Python 2.7 et 3.1, vous pouvez légèrement mieux boucler vous-même sur les résultats chaînés et utiliser dict
ou collections.defaultdict(int)
pour compter (car Counter
est implémenté dans Python, ce qui peut le ralentir dans certains cas), mais laissez Counter
choisir le travail est plus simple et s'auto-documente (je veux dire, l'objectif entier compte, utilisez donc un Counter
). Au-delà de cela, sur CPython (l'interpréteur de référence) 3.2 et supérieur, Counter
dispose d'un accélérateur de niveau C pour compter les entrées pouvant être itératives, qui s'exécutera plus rapidement que tout ce que vous pourriez écrire en Python pur.
Mise à jour: Vous semblez vouloir supprimer la ponctuation et l'insensibilité à la casse, voici donc une variante de mon code précédent qui le fait:
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))
Votre code est beaucoup plus lent, car il crée et détruit de nombreux petits objets Counter
et set
, plutôt que .update
- une seule Counter
une fois par ligne (ce qui, même s'il est légèrement plus lent que ce que j'ai indiqué dans le bloc de code mis à jour, serait au moins algorithmiquement facteur d'échelle similaire).
Une méthode efficace et efficace pour la mémoire consiste à utiliser
scikit
(pour l'extraction de ngram)Word_tokenize
numpy
somme somme pour collecter les comptescollections.Counter
pour la collecte des chiffres et du vocabulaireUn exemple:
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))
[en dehors]:
[(',', 32000),
('.', 17783),
('de', 11225),
('a', 7197),
('que', 5710),
('la', 4732),
('je', 4304),
('se', 4013),
('на', 3978),
('na', 3834)]
Essentiellement, vous pouvez aussi faire ceci:
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)
[en dehors]:
5.257147789001465
Notez que CountVectorizer
peut également prendre un fichier à la place d'une chaîne et tn'est pas nécessaire de lire le fichier entier en mémoire. Dans du code:
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))
Voici un point de repère. Ça va paraître étrange mais le code le plus grossier gagne.
[code]:
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
[en dehors]:
38.306814909
24.8241138458
12.1182529926
Taille de données (154 Mo) utilisée dans le repère ci-dessus:
$ wc -c /path/to/file
161680851
$ wc -l /path/to/file
2176141
Quelques points à noter:
sklearn
, il y a une surcharge de création de vectoriseur + manipulation de numpy et conversion en un objet Counter
Counter
native, il semble que Counter.update()
soit une opération coûteuseCela devrait suffire.
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
vous pouvez essayer avec 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)
Au lieu de décoder les octets entiers lus à partir de l'URL, je traite les données binaires. Étant donné que bytes.translate
s'attend à ce que son deuxième argument soit une chaîne d'octets, I utf-8 code punctuation
. Après avoir enlevé les ponctuations, j'ai utf-8 décoder la chaîne d'octets.
La fonction freq_dist
attend un itérable. C'est pourquoi j'ai passé 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))
Sortie;
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)]
Il semble que dict
soit plus efficace que Counter
object.
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])
Sortie;
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)]
Pour être plus efficace en termes de mémoire lors de l’ouverture d’un fichier de grande taille, vous devez uniquement transmettre l’URL ouverte. Mais le timing inclura également le temps de téléchargement de fichier.
data = urlopen(url)
Word_dist = freq_dist(data)
Sautez CountVectorizer et scikit-learn.
Le fichier est peut-être trop volumineux pour être chargé en mémoire, mais je doute que le dictionnaire python devienne trop volumineux. L'option la plus simple pour vous consiste peut-être à diviser le fichier volumineux en 10 à 20 fichiers plus petits et à étendre votre code pour qu'il passe en boucle par-dessus les fichiers plus petits.