web-dev-qa-db-fra.com

trouver n mots les plus fréquents dans un fichier

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?

34
Lukasz Madon

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".

50
Bruce Ediger

Cela fonctionne mieux avec utf-8:

$ sed -e 's/\s/\n/g' < test.txt | sort | uniq -c | sort -nr | head  -10
9
Vladislav Schogol

Utilisons AWK!

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

7
Sheharyar

Utilisons Haskell!

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
4
BlackCap

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:

  • bible32 est la Bible concaténée avec elle-même 32 fois (135 Mo), bible256 - 256 fois respectivement (1,1 Go).
  • Le ralentissement non linéaire des scripts Python est uniquement dû au fait qu'il traite les fichiers complètement en mémoire, de sorte que les frais généraux augmentent pour les fichiers volumineux.
  • S'il y avait un outil Unix qui pouvait construire un tas et choisir n éléments du haut du tas, la solution AWK pourrait atteindre une complexité temporelle presque linéaire, alors qu'elle est actuellement O (N + Q log Q).
3
Andriy Makukha

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.

3
Reut Sharabani