trova n parole più frequenti in un file


34

Voglio trovare, diciamo, le 10 parole più comuni in un file di testo. In primo luogo, la soluzione dovrebbe essere ottimizzata per le sequenze di tasti (in altre parole, il mio tempo). In secondo luogo, per lo spettacolo. Ecco cosa ho finora per ottenere la 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

Potrei creare un programma java, python ecc. In cui memorizzo (word, numberOfOccurences) in un dizionario e classifico il valore o potrei usare MapReduce, ma ottimizzo per i tasti.

Ci sono falsi positivi? Esiste un modo migliore?


perché dovresti mettere un -10 alla fine? : P
anu,

Risposte:


48

Questo è praticamente il modo più comune di trovare "N cose più comuni", tranne per il fatto che ti manca un sort, e hai un omaggio cat:

tr -c '[:alnum:]' '[\n*]' < test.txt | sort | uniq -c | sort -nr | head  -10

Se non si inserisce un sortprima uniq -c , probabilmente si otterranno molte parole singleton false. uniqesegue solo linee di linee uniche, non uniquness complessive.

EDIT: ho dimenticato un trucco, "stop words". Se stai guardando il testo inglese (scusate, monolingue nordamericano qui), parole come "of", "e", "the" occupano quasi sempre i primi due o tre posti. Probabilmente vuoi eliminarli. La distribuzione GNU Groff contiene un file eignche contiene un elenco abbastanza decente di parole stop. La mia distribuzione Arch ha /usr/share/groff/current/eign, ma penso di aver visto anche /usr/share/dict/eigno /usr/dict/eignnei vecchi Unix.

Puoi usare parole stop come questa:

tr -c '[:alnum:]' '[\n*]' < test.txt |
fgrep -v -w -f /usr/share/groff/current/eign |
sort | uniq -c | sort -nr | head  -10

La mia ipotesi è che la maggior parte delle lingue umane necessiti di simili "stop words" rimosse dai conteggi significativi della frequenza delle parole, ma non so dove suggerire che altre lingue impediscano gli elenchi di parole.

EDIT: fgrep dovrebbe usare il -wcomando, che abilita la corrispondenza di parole intere. Questo evita falsi positivi su parole che contengono semplicemente opere a breve sosta, come "a" o "i".


2
Non cataggiungere un po 'significativo sovraccarico delle prestazioni? Mi piace la sintassi della pipe. Cosa fa * in '[\ n *]'?
Lukasz Madon,

1
Se ti piace il "cat test.txt", allora usalo. Ho letto un articolo da qualche parte in cui Dennis Ritchie afferma che la sintassi "cat qualcosa | qualcosa" è più ampiamente utilizzata e che la sintassi "<qualcosa" è stata qualcosa di un errore, dal momento che ha un unico scopo.
Bruce Ediger,

Cosa succede se voglio trovare il nome della directory più comune in un findoutput? Cioè, dividi le parole /anziché i caratteri degli spazi bianchi e simili.
erb

1
@erb - probabilmente faresti qualcosa del genere:find somewhere optoins | tr '/' '\n' | sort | uniq -c | sort -k1.1nr | head -10
Bruce Ediger,

1
@erb: ponilo come una domanda, non in un commento. Avrai più spazio per inquadrare la tua domanda, così da ottenere la risposta di cui hai bisogno. Fornisci input di esempio e output desiderato. Potresti ottenere alcuni punti reputazione per aver fatto una buona domanda, e io otterrò punti per dare una risposta migliore di quella che posso in un commento.
Bruce Ediger,

9

Funziona meglio con utf-8:

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

7

Usiamo AWK!

Questa funzione elenca la frequenza di ciascuna parola presente nel file fornito in ordine decrescente:

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
}

Puoi chiamarlo sul tuo file in questo modo:

$ cat your_file.txt | wordfrequency

e per le prime 10 parole:

$ cat your_file.txt | wordfrequency | head -10

Fonte: AWK-ward Ruby


4

Usiamo Haskell!

Si sta trasformando in una guerra linguistica, no?

import Data.List
import Data.Ord

main = interact $ (=<<) (\x -> show (length x) ++ " - " ++ head x ++ "\n")
                . sortBy (flip $ comparing length)
                . group . sort
                . words

Uso:

cat input | wordfreq

In alternativa:

cat input | wordfreq | head -10

una versione modificata ignorando il caso: pastebin.com/57T5B6BY
Axel Latvala,

Funziona molto più lentamente del classico sort | uniq -c | sort -nr.
Andriy Makukha,

@AndriyMakukha Il collo di bottiglia è che le stringhe sono elenchi collegati di caratteri in Haskell. Potremmo ottenere velocità simili a C passando a Texto ByteStringinvece, il che è semplice come importarlo qualificato e aggiungere il prefisso alle funzioni con il qualificatore.
BlackCap

pastebin.com/QtJjQwT9 versione significativamente più veloce, scritta per essere leggibile
BlackCap

3

Qualcosa del genere dovrebbe funzionare usando Python che è comunemente disponibile:

cat slowest-names.log | python -c 'import collections, sys; print collections.Counter(sys.stdin);'

Questo presuppone una parola per riga. Se ce ne sono di più, anche la divisione dovrebbe essere facile.


python3 e un output migliorecat README.md | python -c 'import collections, sys, pprint; pprint.pprint(collections.Counter(sys.stdin));'
Lukasz Madon,

2

Questo è un classico problema che risuonò nel 1986, quando Donald Knuth implementò una soluzione rapida con tentativi di hash in un programma di 8 pagine per illustrare la sua tecnica di programmazione letteraria, mentre Doug McIlroy, il padrino delle pipe Unix, rispose con un one-liner, non è stato così veloce, ma ha fatto il lavoro:

tr -cs A-Za-z '\n' | tr A-Z a-z | sort | uniq -c | sort -rn | sed 10q

Naturalmente, la soluzione di McIlroy ha una complessità temporale O (N log N), dove N è un numero totale di parole. Ci sono soluzioni molto più veloci. Per esempio:

Ecco un'implementazione C ++ con la complessità del tempo limite superiore O ((N + k) log k), in genere - quasi lineare.

Di seguito è una veloce implementazione di Python che utilizza dizionari hash e heap con complessità temporale O (N + k log Q), dove Q è un numero di parole uniche:

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)

Ecco una soluzione estremamente veloce in Rust di Anders Kaseorg.

Confronto del tempo della CPU (in secondi):

                                     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

Gli appunti:

  • bible32 è la Bibbia concatenata con se stessa 32 volte (135 MB), bibbia256 - 256 volte rispettivamente (1,1 GB).
  • Il rallentamento non lineare degli script Python è causato esclusivamente dal fatto che elabora i file completamente in memoria, quindi le spese generali stanno diventando più grandi per i file di grandi dimensioni.
  • Se esistesse uno strumento Unix in grado di costruire un heap e selezionare n elementi dall'alto dell'heap, la soluzione AWK potrebbe raggiungere una complessità temporale quasi lineare, mentre attualmente è O (N + Q log Q).
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.