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

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

8 ответов


прежде всего, я определяю 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;
     }
 }

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

#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;
}

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


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

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] );
                }
            }
        }

    }

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

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 аварийных служб" (я, вероятно, хотел бы, чтобы это считалось как три слова).

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


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 GB с 580,000,000 слова.
Perl 5.22 завершен менее чем за 3 минуты.


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


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

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

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

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

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


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;
}