найди самые частые слова в файле

34

Я хочу найти, скажем, 10 самых распространенных слов в текстовом файле. Во-первых, решение должно быть оптимизировано для нажатия клавиш (другими словами - мое время). Во-вторых, для исполнения. Вот что у меня есть, чтобы получить топ-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

Я мог бы создать программу на языке Java, Python и т. Д., В которой я храню (word, numberOfOccurences) в словаре и сортирую значение, или я мог бы использовать MapReduce, но я оптимизирую нажатия клавиш.

Есть ли ложные срабатывания? Есть ли способ лучше?

Лукаш Мадон
источник
зачем тебе ставить -10 в конце? : P
Anu

Ответы:

48

Это довольно распространенный способ найти «N самых распространенных вещей», за исключением того, что вы пропустили a sort, и у вас есть подарок cat:

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

Если вы не вставите sortперед этим, uniq -c вы, вероятно, получите много ложных синглтон-слов. uniqделает только уникальные линии, а не уникальность.

РЕДАКТИРОВАТЬ: я забыл трюк, "стоп-слова". Если вы смотрите на текст на английском языке (извините, здесь говорят на одноязычном языке для Северной Америки), такие слова, как "of", "и", "the", почти всегда занимают верхние два или три места. Вы, вероятно, хотите устранить их. В дистрибутиве GNU Groff есть названный файл, eignкоторый содержит довольно приличный список стоп-слов. Мой Arch дистрибутив есть /usr/share/groff/current/eign, но я думаю, что я также видел /usr/share/dict/eignили /usr/dict/eignв старых Unixes.

Вы можете использовать стоп-слова, как это:

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

Я предполагаю, что большинство человеческих языков нуждаются в аналогичных «стоп-словах», удаленных из значимых значений частоты слов, но я не знаю, где предложить другие языки, чтобы остановить список слов.

РЕДАКТИРОВАТЬ: fgrep следует использовать -wкоманду, которая включает сопоставление целых слов. Это позволяет избежать ложных срабатываний в словах, которые просто содержат короткие остановки, такие как «а» или «я».

Брюс Эдигер
источник
2
Добавляет ли это catсущественные потери производительности? Мне нравится синтаксис канала. Что делает * in '[\ n *]'?
Лукаш Мадон
1
Если вам нравится "cat test.txt", то обязательно используйте его. Я где-то читал статью, в которой Деннис Ритчи говорит, что синтаксис «что-то | что-то еще» более широко используется, и что синтаксис «что-то» был чем-то ошибочным, поскольку он был единственной целью.
Брюс Эдигер
Что если я хочу найти наиболее распространенное имя каталога в findвыводе? То есть разделять слова /вместо пробельных символов и тому подобное.
erb
1
@erb - вы, вероятно, сделали бы что-то вроде:find somewhere optoins | tr '/' '\n' | sort | uniq -c | sort -k1.1nr | head -10
Брюс Эдигер
1
@erb - задайте это как вопрос, а не как комментарий. У вас будет больше возможностей сформулировать свой вопрос, чтобы получить нужный ответ. Приведите пример ввода и желаемый результат. Вы можете получить очки репутации за хороший вопрос, а я получу очки за лучший ответ, чем могу в комментарии.
Брюс Эдигер
9

Это работает лучше с utf-8:

$ sed -e 's/\s/\n/g' < test.txt | sort | uniq -c | sort -nr | head  -10
Владислав Шоголь
источник
7

Давайте использовать AWK!

Эта функция перечисляет частоту каждого слова, встречающегося в предоставленном файле в порядке убывания:

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
}

Вы можете назвать это в своем файле следующим образом:

$ cat your_file.txt | wordfrequency

и для лучших 10 слов:

$ cat your_file.txt | wordfrequency | head -10

Источник: AWK-опека Руби

Sheharyar
источник
4

Давайте использовать Haskell!

Это превращается в языковую войну, не так ли?

import Data.List
import Data.Ord

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

Использование:

cat input | wordfreq

В качестве альтернативы:

cat input | wordfreq | head -10
судейская шапочка
источник
измененная версия, игнорирующая случай: pastebin.com/57T5B6BY
Аксель Латвала
Работает намного медленнее, чем классика sort | uniq -c | sort -nr.
Андрей Макуха
@AndriyMakukha Узким местом является то, что строки - это связанные списки символов в Haskell. Мы могли бы получить C-подобные скорости, переключаясь на Textили ByteStringвместо этого, что так же просто, как импортировать квалифицированную и ставить префикс перед функциями с квалификатором.
BlackCap
pastebin.com/QtJjQwT9 значительно более быстрая версия, написанная для удобства чтения
BlackCap
3

Примерно так должно работать с использованием общедоступного python:

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

Это предполагает слово в строке. Если их больше, расщепление должно быть легким.

Реут Шарабани
источник
выход python3 и лучшеcat README.md | python -c 'import collections, sys, pprint; pprint.pprint(collections.Counter(sys.stdin));'
Лукаш Мадон
2

Это классическая проблема, которая получила некоторый резонанс в 1986 году, когда Дональд Кнут реализовал быстрое решение с помощью попыток хэширования в программе длиной в 8 страниц, чтобы проиллюстрировать свою технику грамотного программирования, в то время как Дуг Макилрой, крестный отец каналов Unix, ответил с Это было не так быстро, но сделало работу:

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

Конечно, решение Макилроя имеет временную сложность O (N log N), где N - общее количество слов. Есть гораздо более быстрые решения. Например:

Вот реализация C ++ с верхней границей сложности времени O ((N + k) log k), обычно - почти линейной.

Ниже приведена быстрая реализация Python с использованием хеш-словарей и кучи с временной сложностью O (N + k log Q), где Q - количество уникальных слов:

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)

Вот это очень быстро раствор в Rust Андерс Kaseorg.

Сравнение времени процессора (в секундах):

                                     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

Заметки:

  • Библия - это Библия, соединенная с самим собой 32 раза (135 МБ), Библия256 - 256 раз соответственно (1,1 ГБ).
  • Нелинейное замедление работы скриптов Python вызвано исключительно тем фактом, что он полностью обрабатывает файлы в памяти, поэтому накладные расходы увеличиваются для больших файлов.
  • Если бы существовал инструмент Unix, который мог бы построить кучу и выбрать n элементов из верхней части кучи, решение AWK могло бы достичь почти линейной сложности по времени, в то время как в настоящее время это O (N + Q log Q).
Андрей Макуха
источник