Каковы элегантные и эффективные способы подсчета частоты каждого "английского" слова в файле?
Элегантные способы подсчета частоты слов в файле
Ответ 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;
}