Je veux trouver, disons, 10 mots les plus courants dans un fichier texte. Tout d'abord, la solution doit être optimisée pour les frappes (en d'autres termes - mon temps). Deuxièmement, pour la performance. Voici ce que j'ai jusqu'à présent pour obtenir le top 10:
cat test.txt | tr -c '[:alnum:]' '[\n*]' | uniq -c | sort -nr | head -10
6 k
2 g
2 e
2 a
1 r
1 k22
1 k
1 f
1 eeeeeeeeeeeeeeeeeeeee
1 d
Je pourrais faire un programme Java, python etc. où je stocke (Word, numberOfOccurences) dans un dictionnaire et trier la valeur ou je pourrais utiliser MapReduce, mais j'optimise pour les frappes.
Y a-t-il des faux positifs? Y a-t-il une meilleure façon?
C'est à peu près la façon la plus courante de trouver "N choses les plus courantes", sauf qu'il vous manque un sort
, et vous avez un cat
gratuit:
tr -c '[:alnum:]' '[\n*]' < test.txt | sort | uniq -c | sort -nr | head -10
Si vous ne mettez pas de sort
avant le uniq -c
vous obtiendrez probablement beaucoup de faux mots singleton. uniq
ne fait que des séries de lignes uniques, pas l'unicité globale.
EDIT: J'ai oublié une astuce, "stop mots". Si vous regardez du texte anglais (désolé, monolingue en Amérique du Nord ici), des mots comme "of", "et", "the" occupent presque toujours les deux ou trois premières places. Vous voulez probablement les éliminer. La distribution GNU Groff contient un fichier nommé eign
qui contient une liste assez décente de mots vides. Ma distribution Arch a /usr/share/groff/current/eign
, mais je pense que j'ai aussi vu /usr/share/dict/eign
ou /usr/dict/eign
dans les anciens Unix.
Vous pouvez utiliser des mots vides comme celui-ci:
tr -c '[:alnum:]' '[\n*]' < test.txt |
fgrep -v -w -f /usr/share/groff/current/eign |
sort | uniq -c | sort -nr | head -10
Je suppose que la plupart des langues humaines ont besoin de "mots vides" similaires supprimés des décomptes significatifs de la fréquence des mots, mais je ne sais pas où suggérer d'obtenir d'autres listes de mots vides.
EDIT:fgrep
devrait utiliser le -w
, qui active la correspondance de mots entiers. Cela évite les faux positifs sur les mots qui contiennent simplement de courts arrêts, comme "a" ou "i".
Cela fonctionne mieux avec utf-8:
$ sed -e 's/\s/\n/g' < test.txt | sort | uniq -c | sort -nr | head -10
Cette fonction répertorie la fréquence de chaque mot apparaissant dans le fichier fourni dans l'ordre décroissant:
function wordfrequency() {
awk '
BEGIN { FS="[^a-zA-Z]+" } {
for (i=1; i<=NF; i++) {
Word = tolower($i)
words[Word]++
}
}
END {
for (w in words)
printf("%3d %s\n", words[w], w)
} ' | sort -rn
}
Vous pouvez l'appeler sur votre fichier comme ceci:
$ cat your_file.txt | wordfrequency
et pour les 10 premiers mots:
$ cat your_file.txt | wordfrequency | head -10
Source: Rubis AWK-ward
Cela se transforme en une guerre des langues, n'est-ce pas?
import Data.List
import Data.Ord
main = interact $ (=<<) (\x -> show (length x) ++ " - " ++ head x ++ "\n")
. sortBy (flip $ comparing length)
. group . sort
. words
Usage:
cat input | wordfreq
Alternativement:
cat input | wordfreq | head -10
C'est un problème classique qui a eu une résonance en 1986, quand Donald Knuth a implémenté une solution rapide avec des tentatives de hachage dans un programme de 8 pages pour illustrer sa technique de programmation lettrée, tandis que Doug McIlroy, le parrain des tuyaux Unix, a répondu avec un seul revêtement, ce n'était pas aussi rapide, mais a fait le travail:
tr -cs A-Za-z '\n' | tr A-Z a-z | sort | uniq -c | sort -rn | sed 10q
Bien sûr, la solution de McIlroy a une complexité temporelle O (N log N), où N est un nombre total de mots. Il existe des solutions beaucoup plus rapides. Par exemple:
Ici est une implémentation C++ avec la complexité temporelle supérieure O ((N + k) log k), généralement - presque linéaire.
Ci-dessous est une implémentation rapide Python utilisant des dictionnaires de hachage et un tas avec une complexité temporelle O (N + k log Q), où Q est un certain nombre de mots uniques:
import collections, re, sys
filename = sys.argv[1]
k = int(sys.argv[2]) if len(sys.argv)>2 else 10
text = open(filename).read()
counts = collections.Counter(re.findall('[a-z]+', text.lower()))
for i, w in counts.most_common(k):
print(i, w)
Ici est une solution extrêmement rapide dans Rust par Anders Kaseorg.
Comparaison du temps CPU (en secondes):
bible32 bible256
Rust (prefix tree) 0.632 5.284
C++ (prefix tree + heap) 4.838 38.587
Python (Counter) 9.851 100.487
Sheharyar (AWK + sort) 30.071 251.301
McIlroy (tr + sort + uniq) 60.251 690.906
Remarques:
Quelque chose comme ça devrait fonctionner en utilisant python qui est couramment disponible:
cat slowest-names.log | python -c 'import collections, sys; print collections.Counter(sys.stdin);'
Cela suppose Word par ligne. S'il y en a plus, le fractionnement devrait également être facile.