皇上,还记得我吗?我就是1999年那个Linux伊甸园啊-----24小时滚动更新开源资讯,全年无休!

使用 awk 统计字母频率

使用 awk 统计字母频率

编写一个 awk 脚本来找到一组单词中出现次数最多(和最少)的单词。

近一段时间,我开始编写一个小游戏,在这个小游戏里,玩家使用一个个字母块来组成单词。编写这个游戏之前,我需要先知道常见英文单词中每个字母的使用频率,这样一来,我就可以找到一组更有用的字母块。字母频次统计在很多地方都有相关讨论,包括在 维基百科 上,但我还是想要自己来实现。

Linux 系统在 /usr/share/dict/words 文件中提供了一个单词列表,所以我已经有了一个现成的单词列表。然而,尽管这个 words 文件包含了很多我想要的单词,却也包含了一些我不想要的。我想要的单词首先不能是复合词(即不包含连接符和空格的单词),也不能是专有名词(即不包含大写字母单词)。为了得到这个结果,我可以运行 grep 命令来取出只由小写字母组成的行:

  1. $ grep  '^[a-z]*$' /usr/share/dict/words

这个正则表达式的作用是让 grep 去匹配仅包含小写字母的行。表达式中的字符 ^ 和 $ 分别代表了这一行的开始和结束。[a-z] 分组仅匹配从 “a” 到 “z” 的小写字母。

下面是一个输出示例:

  1. $ grep  '^[a-z]*$' /usr/share/dict/words | head
  2. a
  3. aa
  4. aaa
  5. aah
  6. aahed
  7. aahing
  8. aahs
  9. aal
  10. aalii
  11. aaliis

没错,这些都是合法的单词。比如,“aahed” 是 “aah” 的过去式,表示在放松时的感叹,而 “aalii” 是一种浓密的热带灌木。

现在我只需要编写一个 gawk 脚本来统计出单词中各个字母出现的次数,然后打印出每个字母的相对频率。

字母计数

一种使用 gawk 来统计字母个数的方式是,遍历每行输入中的每一个字符,然后对 “a” 到 “z” 之间的每个字母进行计数。substr 函数会返回一个给定长度的子串,它可以只包含一个字符,也可以是更长的字符串。比如,下面的示例代码能够取到输入中的每一个字符 c

  1. {
  2. len = length($0); for (i = 1; i <= len; i++) {
  3. c = substr($0, i, 1);
  4. }
  5. }

如果使用一个全局字符串变量 LETTERS 来存储字母表,我就可以借助 index 函数来找到某个字符在字母表中的位置。我将扩展 gawk 代码示例,让它在输入数据中只取范围在 “a” 到 “z” 的字母:

  1. BEGIN { LETTERS = "abcdefghijklmnopqrstuvwxyz" }
  2. {
  3. len = length($0); for (i = 1; i <= len; i++) {
  4. c = substr($0, i, 1);
  5. ltr = index(LETTERS, c);
  6. }
  7. }

需要注意的是,index 函数将返回字母在 LETTERS 字符串中首次出现的位置,第一个位置返回 1,如果没有找到则返回 0。如果我有一个大小为 26 的数组,我就可以利用这个数组来统计每个字母出现的次数。我将在下面的示例代码中添加这个功能,每当一个字母出现在输入中,我就让它对应的数组元素值增加 1(使用 ++):

  1. BEGIN { LETTERS = "abcdefghijklmnopqrstuvwxyz" }
  2.  
  3. {
  4.     len = length($0); for (i = 1; i <= len; i++) {
  5.         c = substr($0, i, 1);
  6.         ltr = index(LETTERS, c);
  7.  
  8.         if (ltr &gt; 0) {
  9.             ++count[ltr];
  10.         }
  11.     }
  12. }

打印相对频率

当 gawk 脚本统计完所有的字母后,我希望它能输出每个字母的频率。毕竟,我对输入中各个字母的个数没有兴趣,我更关心它们的 相对频率

我将先统计字母 “a” 的个数,然后把它和剩余 “b” 到 “z” 字母的个数比较:

  1. END {
  2.     min = count[1]; for (ltr = 2; ltr <= 26; ltr++) {
  3.         if (count[ltr] < min) {
  4.             min = count[ltr];
  5.         }
  6.     }
  7. }

在循环的最后,变量 min 会等于最少的出现次数,我可以把它为基准,为字母的个数设定一个参照值,然后计算打印出每个字母的相对频率。比如,如果出现次数最少的字母是 “q”,那么 min 就会等于 “q” 的出现次数。

接下来,我会遍历每个字母,打印出它和它的相对频率。我通过把每个字母的个数都除以 min 的方式来计算出它的相对频率,这意味着出现次数最少的字母的相对频率是 1。如果另一个字母出现的次数恰好是最少次数的两倍,那么这个字母的相对频率就是 2。我只关心整数,所以 2.1 和 2.9 对我来说是一样的(都是 2)。

  1. END {
  2.     min = count[1]; for (ltr = 2; ltr <= 26; ltr++) {
  3.         if (count[ltr] < min) {
  4.             min = count[ltr];
  5.         }
  6.     }
  7.  
  8.     for (ltr = 1; ltr <= 26; ltr++) {
  9.         print substr(LETTERS, ltr, 1), int(count[ltr] / min);
  10.     }
  11. }

最后的完整程序

现在,我已经有了一个能够统计输入中各个字母的相对频率的 gawk 脚本:

  1. #!/usr/bin/gawk -f
  2.  
  3. # 只统计 a-z 的字符,忽略 A-Z 和其他的字符
  4.  
  5. BEGIN { LETTERS = "abcdefghijklmnopqrstuvwxyz" }
  6.  
  7. {
  8.     len = length($0); for (i = 1; i <= len; i++) {
  9.         c = substr($0, i, 1);
  10.         ltr = index(LETTERS, c);
  11.  
  12.         if (ltr < 0) {
  13.             ++count[ltr];
  14.         }
  15.     }
  16. }
  17.  
  18. # 打印每个字符的相对频率
  19.    
  20. END {
  21.     min = count[1]; for (ltr = 2; ltr <= 26; ltr++) {
  22.         if (count[ltr] < min) {
  23.             min = count[ltr];
  24.         }
  25.     }
  26.  
  27.     for (ltr = 1; ltr <= 26; ltr++) {
  28.         print substr(LETTERS, ltr, 1), int(count[ltr] / min);
  29.     }
  30. }

我将把这段程序保存到名为 letter-freq.awk 的文件中,这样一来,我就可以在命令行中更方便地使用它。

如果你愿意的话,你也可以使用 chmod +x 命令把这个文件设为可独立执行。第一行中的 #!/usr/bin/gawk -f 表示 Linux 会使用 /usr/bin/gawk 把这个文件当作一个脚本来运行。由于 gawk 命令行使用 -f 来指定它要运行的脚本文件名,你需要在末尾加上 -f。如此一来,当你在 shell 中执行 letter-freq.awk,它会被解释为 /usr/bin/gawk -f letter-freq.awk

接下来我将用几个简单的输入来测试这个脚本。比如,如果我给我的 gawk 脚本输入整个字母表,每个字母的相对频率都应该是 1:

  1. $ echo abcdefghijklmnopqrstuvwxyz | gawk -f letter-freq.awk
  2. a 1
  3. b 1
  4. c 1
  5. d 1
  6. e 1
  7. f 1
  8. g 1
  9. h 1
  10. i 1
  11. j 1
  12. k 1
  13. l 1
  14. m 1
  15. n 1
  16. o 1
  17. p 1
  18. q 1
  19. r 1
  20. s 1
  21. t 1
  22. u 1
  23. v 1
  24. w 1
  25. x 1
  26. y 1
  27. z 1

还是使用上述例子,只不过这次我在输入中添加了一个字母 “e”,此时的输出结果中,“e” 的相对频率会是 2,而其他字母的相对频率仍然会是 1:

  1. $ echo abcdeefghijklmnopqrstuvwxyz | gawk -f letter-freq.awk
  2. a 1
  3. b 1
  4. c 1
  5. d 1
  6. e 2
  7. f 1
  8. g 1
  9. h 1
  10. i 1
  11. j 1
  12. k 1
  13. l 1
  14. m 1
  15. n 1
  16. o 1
  17. p 1
  18. q 1
  19. r 1
  20. s 1
  21. t 1
  22. u 1
  23. v 1
  24. w 1
  25. x 1
  26. y 1
  27. z 1

现在我可以跨出最大的一步了!我将使用 grep 命令和 /usr/share/dict/words 文件,统计所有仅由小写字母组成的单词中,各个字母的相对使用频率:

  1. $ grep  '^[a-z]*$' /usr/share/dict/words | gawk -f letter-freq.awk
  2. a 53
  3. b 12
  4. c 28
  5. d 21
  6. e 72
  7. f 7
  8. g 15
  9. h 17
  10. i 58
  11. j 1
  12. k 5
  13. l 36
  14. m 19
  15. n 47
  16. o 47
  17. p 21
  18. q 1
  19. r 46
  20. s 48
  21. t 44
  22. u 25
  23. v 6
  24. w 4
  25. x 1
  26. y 13
  27. z 2

在 /usr/share/dict/words 文件的所有小写单词中,字母 “j”、“q” 和 “x” 出现的相对频率最低,字母 “z” 也使用得很少。不出意料,字母 “e” 是使用频率最高的。


via: https://opensource.com/article/21/4/gawk-letter-game

作者:Jim Hall 选题:lujun9972 译者:lkxed 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

转自 技术|使用 awk 统计字母频率 (linux.cn)