优雅的方法来统计文件中单词的频率

12

有哪些优雅有效的方法可以在文件中统计每个“英语”单词的频率?


8
请定义“单词”。您是指“英语单词”还是“连续的字母字符序列”或“连续的字符序列”,还是其他什么? - James McNellis
为什么目的 - 只是为了好玩吗? - tenfour
重复?那么,“他去商店”算作1次重复,还是算作5个独特单词中有一个计数为2的重复单词? - Fred Nurk
1
缩略词和所有格词算不算?例如,can'tThe cat's toy. - Thomas Matthews
1
这些字母序列必须是有效的英语单词吗?例如,a是一个有效的单词,但t不是。 - Thomas Matthews
显示剩余3条评论
8个回答

16

首先,我定义了 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;
     }
 }

1
我认为这是正确的答案,因为他想要重复单词的频率。 - Murilo Vasconcelos
1
第一个解决方案中的输入循环是错误的。eof标志在由于到达eof而失败的输入操作之后设置。 - James McNellis
再次强调,这__不是__正确的答案。OP并__没有__要求以空格分隔的单词。这将把"end_of_sentence.""end_of_sentence!"视为__两个不同的单词__,这__不是__OP想要的。 - sbi
1
@Nawaz:为什么不直接使用惯用的 while (input >> word)?虽然其他标志位没有被检查,但原文写法仍然是错误的。 - James McNellis
@Nawaz,我在你的代码中发现了一个错误。在将单词添加到映射之前,你需要调用 tolower 函数(请查看我的解决方案)。 - UmmaGumma
显示剩余13条评论

4
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 我在一个有580,000,000个单词的3.3GB文本文件上运行了这段代码。 Perl 5.22在不到3分钟内完成了操作。

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函数。


这对于英语肯定有效(这是OP所要求的,我知道),但对于其他语言则不然。如果输入文本中有数字,它也无法正常工作。 - Baltasarq
@Baltasarq的问题是关于“英语”单词。另外,is_alpha函数对于数字不会返回true。 - UmmaGumma

2
我的解决方案如下。首先,所有符号都被转换为空格。然后,基本上使用此前提供的解决方案来提取单词:
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] );
                }
            }
        }

    }

我已经改进了算法并修复了一些小错误。 - Baltasarq

1

以下是我认为接近您所需的算法的伪代码:

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 km”,具体视您的需求而定。漏报单词包括“911 emergency services”(我可能希望将其视为三个单词)。

简而言之,自然语言解析很难:根据您的实际用例,您可能可以使用一些近似值。


2
回答C++问题的有趣方式:提供Python代码,然后声明它是伪代码。考虑到这里使用了Python stdlib中的类型而没有导入它,以及理解推导式,任何阅读此内容的C++开发人员都必须猜测很多,我很惊讶这个回答竟然得到了赞同。也许这是一个秘密实验,看看有多少C++程序员可以在不知不觉中被转化为Python爱好者? - cfi

1
  1. 确定“英语单词”的确切含义。定义应涵盖以下内容,如“able-bodied”是一个单词还是两个单词,如何处理撇号(“Don't trust 'em!”),大写是否重要等。

  2. 创建一组测试用例,以确保正确执行步骤1中的所有决策。

  3. 创建一个标记生成器,从输入中读取下一个单词(由步骤1定义),并以标准形式返回它。根据您的定义,这可能是一个简单的状态机、正则表达式或仅依赖于<istream>的提取运算符(例如,std::cin >> word;)。使用步骤2中的所有测试用例测试您的标记生成器。

  4. 选择一种数据结构来保存单词和计数。在现代C++中,您可能会得到像std::map<std::string, unsigned>std::unordered_map<std::string, int>这样的东西。

  5. 编写循环,从标记生成器获取下一个单词,并递增其直方图中的计数,直到输入中没有更多单词为止。


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

0

另一种简单的方法是计算文件中空格的数量,直到发现超过一个空格,如果您只考虑单词之间的单个空格...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接