Элегантные способы подсчета частоты слов в файле

Каковы элегантные и эффективные способы подсчета частоты каждого "английского" слова в файле?

Ответ 1

Прежде всего, я определяю letter_only std::locale, чтобы игнорировать пунктуации, исходящие из потока, и читать только действительные "английские" буквы из входного потока. Таким образом, поток будет обрабатывать слова "ways", "ways." и "ways!" как одно и то же слово "ways", потому что поток будет игнорировать пунктуации, такие как "." и "!".

struct letter_only: std::ctype<char> 
{
    letter_only(): std::ctype<char>(get_table()) {}

    static std::ctype_base::mask const* get_table()
    {
        static std::vector<std::ctype_base::mask> 
            rc(std::ctype<char>::table_size,std::ctype_base::space);

        std::fill(&rc['A'], &rc['z'+1], std::ctype_base::alpha);
        return &rc[0];
    }
};

Решение 1

int main()
{
     std::map<std::string, int> wordCount;
     ifstream input;
     input.imbue(std::locale(std::locale(), new letter_only())); //enable reading only letters!
     input.open("filename.txt");
     std::string word;
     while(input >> word)
     {
         ++wordCount[word];
     }
     for (std::map<std::string, int>::iterator it = wordCount.begin(); it != wordCount.end(); ++it)
     {
           cout << it->first <<" : "<< it->second << endl;
     }
}

Решение 2

struct Counter
{
    std::map<std::string, int> wordCount;
    void operator()(const std::string & item) { ++wordCount[item]; }
    operator std::map<std::string, int>() { return wordCount; }
};

int main()
{
     ifstream input;
     input.imbue(std::locale(std::locale(), new letter_only())); //enable reading only letters!
     input.open("filename.txt");
     istream_iterator<string> start(input);
     istream_iterator<string> end;
     std::map<std::string, int> wordCount = std::for_each(start, end, Counter());
     for (std::map<std::string, int>::iterator it = wordCount.begin(); it != wordCount.end(); ++it)
     {
          cout << it->first <<" : "<< it->second << endl;
     }
 }

Ответ 2

Вот работающее решение. Это должно работать с реальным текстом (включая пунктуацию):

#include <iterator>
#include <iostream>
#include <fstream>
#include <map>
#include <string>
#include <cctype>

std::string getNextToken(std::istream &in)
{
    char c;
    std::string ans="";
    c=in.get();
    while(!std::isalpha(c) && !in.eof())//cleaning non letter charachters
    {
        c=in.get();
    }
    while(std::isalpha(c))
    {
        ans.push_back(std::tolower(c));
        c=in.get();
    }
    return ans;
}

int main()
{
    std::map<std::string,int> words;
    std::ifstream fin("input.txt");

    std::string s;
    std::string empty ="";
    while((s=getNextToken(fin))!=empty )
            ++words[s];

    for(std::map<std::string,int>::iterator iter = words.begin(); iter!=words.end(); ++iter)
        std::cout<<iter->first<<' '<<iter->second<<std::endl;
}

Изменить: теперь мой код вызывает tolower для каждой буквы.

Ответ 3

Мое решение следующее. Во-первых, все символы преобразуются в пробелы. Затем, в основном, такое же решение, представленное здесь ранее, используется для извлечения слов:

const std::string Symbols = ",;.:-()\t!¡¿?\"[]{}&<>+-*/=#'";
typedef std::map<std::string, unsigned int> WCCollection;
void countWords(const std::string fileName, WCCollection &wcc)
    {
        std::ifstream input( fileName.c_str() );

        if ( input.is_open() ) {
            std::string line;
            std::string word;

            while( std::getline( input, line ) ) {
                // Substitute punctuation symbols with spaces
                for(std::string::const_iterator it = line.begin(); it != line.end(); ++it) {
                    if ( Symbols.find( *it ) != std::string::npos ) {
                        *it = ' ';
                    }

                }

                // Let std::operator>> separate by spaces
                std::istringstream filter( line );
                while( filter >> word ) {
                    ++( wcc[word] );
                }
            }
        }

    }

Ответ 4

Псевдокод для алгоритма, который, как я полагаю, близок к тому, что вы хотите:

counts = defaultdict(int)
for line in file:
  for word in line.split():
    if any(x.isalpha() for x in word):
      counts[word.toupper()] += 1

freq = sorted(((count, word) for word, count in counts.items()), reversed=True)
for count, word in freq:
  print "%d\t%s" % (count, word)

Нечувствительное к регистру сравнение обрабатывается наивно и, вероятно, сочетает слова, которые вы не хотите комбинировать в абсолютно общем смысле. Будьте осторожны с символами, отличными от ASCII, в вашей реализации вышеизложенного. Ложные срабатывания могут включать "1-800-555-TELL", "0xDEADBEEF" и "42 км", в зависимости от того, что вы хотите. Пропущенные слова включают "911 экстренных служб" (я бы, наверное, хотел, чтобы это было три слова).

Короче говоря, синтаксический анализ на естественном языке сложный: вы, вероятно, можете сделать это с некоторым приближением в зависимости от вашего фактического использования.

Ответ 5

Perl, возможно, не настолько изящный, но очень эффективный.
Я разместил решение здесь: Обработка огромных текстовых файлов

В двух словах,

1) При необходимости разделите знаки пунктуации и перепишите в верхнем регистре в нижний регистр:
perl -pe "s/[^a-zA-Z \t\n']/ /g; tr/A-Z/a-z/" file_raw > file

2) Подсчитайте появление каждого слова. Результаты печати отсортированы сначала по частоте, а затем по алфавиту:
perl -lane '$h{$_}++ for @F; END{for $w (sort {$h{$b}<=>$h{$a} || $a cmp $b} keys %h) {print "$h{$w}\t$w"}}' file > freq

Я запустил этот код в текстовом файле объемом 3,3 ГБ с 580 000 000 слов.
Perl 5.22 завершен менее чем за 3 минуты.

Ответ 6

Еще один простой способ - подсчитать количество пробелов в файле до тех пор, пока не будет найдено одно место, если вы считаете только одно пробел между словами...

Ответ 7

  • Решите, что именно вы подразумеваете под "английским словом". Определение должно охватывать такие вещи, как "трудоспособное" - это одно слово или два, как обрабатывать апострофы ( "Не доверяйте им!" ), Значительна ли капитализация и т.д.

  • Создайте набор тестовых примеров, чтобы вы могли убедиться, что все решения на шаге 1 верны.

  • Создайте токенизатор, который считывает следующее слово (как определено на шаге 1) из ввода и возвращает его в стандартной форме. В зависимости от того, как ваше определение может быть простым конечным автоматом, регулярным выражением или просто полагаться на операторы извлечения <istream> (например, std::cin >> word;). Проверьте свой токенизатор со всеми тестовыми примерами с шага 2.

  • Выберите структуру данных для хранения слов и отсчетов. В современном С++ вы, вероятно, получите что-то вроде std::map<std::string, unsigned> или std::unordered_map<std::string, int>.

  • Напишите цикл, который получает следующее слово от токенизатора и увеличивает его количество в гистограмме, пока на входе больше слов.

Ответ 8

string mostCommon( string filename ) {

    ifstream input( filename );
    string line;
    string mostFreqUsedWord;
    string token;
    map< string, int > wordFreq;

    if ( input.is_open() ) {

        while ( true ) {
            input >> token;
            if( input ) {
                wordFreq[ token ]++;
                if ( wordFreq[ token] > wordFreq[ mostFreqUsedWord ] )
                    mostFreqUsedWord = token;
            } else
                break;
        }
        input.close();
    } else {
        cout << "Unable to ope file." << endl;
    }
    return mostFreqUsedWord;
}