网站搜索

使用awk计算字母频率


编写一个 awk 脚本来确定一组单词中最常见(和最不常见)的字母。

我最近开始编写一个游戏,您可以使用字母图块来构建单词。为了创建游戏,我需要知道英语中常规单词中字母的频率,以便我可以呈现一组有用的字母图块。字母频率在很多地方都有讨论,包括维基百科,但我想自己计算字母频率。

Linux 在 /usr/share/dict/words 文件中提供了单词列表,因此我已经有了可能使用的单词列表。 words 文件包含很多我想要的单词,但也有一些我不需要的单词。我想要一个不是复合词(没有连字符或空格)或专有名词(没有大写字母)的所有单词的列表。要获取该列表,我可以运行 grep 命令来仅提取仅由小写字母组成的行:

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

此正则表达式要求 grep 匹配仅包含小写字母的模式。模式中的字符 ^ 和 $ 分别表示行的开头和结尾。 [a-z] 分组将仅匹配小写字母 a 到 z。

以下是输出的快速示例:

$ grep  '^[a-z]*$' /usr/share/dict/words | head
a
aa
aaa
aah
aahed
aahing
aahs
aal
aalii
aaliis

是的,这些都是有效的话。例如,“aahed”是“aah”的过去时感叹词,就像放松时一样。 “aalii”是一种茂密的热带灌木。

现在我只需要编写一个 gawk 脚本来完成计算每个单词中的字母的工作,然后打印它找到的每个字母的相对频率。

数字母

在 gawk 中计算字母的一种方法是迭代每个输入行中的每个字符,并计算每个字母 a 到 z 的出现次数。 substr 函数将从较大的字符串中返回给定长度的子字符串,例如单个字母。例如,此代码示例将评估输入中的每个字符 c:

{
    len = length($0); for (i = 1; i <= len; i++) {
        c = substr($0, i, 1);
    }
}

如果我从包含字母表的全局字符串 LETTERS 开始,我可以使用 index 函数来查找字母表中单个字母的位置。我将扩展 gawk 代码示例,以仅计算输入中的字母 a 到 z:

BEGIN { LETTERS = "abcdefghijklmnopqrstuvwxyz" }
 
{
    len = length($0); for (i = 1; i <= len; i++) {
        c = substr($0, i, 1);
        ltr = index(LETTERS, c);
    }
}

请注意,索引函数返回 LETTERS 字符串中第一次出现的字母,第一个字母从 1 开始,如果没有找到则返回零。如果我有一个 26 个元素长的数组,我可以使用该数组来计算每个字母的出现次数。我会将其添加到我的代码示例中,以增加(使用 ++)输入中出现的每个字母的计数:

BEGIN { LETTERS = "abcdefghijklmnopqrstuvwxyz" }
 
{
    len = length($0); for (i = 1; i <= len; i++) {
        c = substr($0, i, 1);
        ltr = index(LETTERS, c);
 
        if (ltr > 0) {
            ++count[ltr];
        }
    }
}

打印相对频率

在 gawk 脚本对所有字母进行计数后,我想打印它找到的每个字母的频率。我对输入中每个字母的总数不感兴趣,而是对每个字母的相对频率感兴趣。相对频率对计数进行缩放,以便将出现次数最少的字母(例如字母 q)设置为 1,而其他字母则相对于此。

我将从字母 a 的计数开始,然后将该值与其他每个字母 b 到 z 的计数进行比较:

END {
    min = count[1]; for (ltr = 2; ltr <= 26; ltr++) {
        if (count[ltr] < min) {
            min = count[ltr];
        }
    }
}

在该循环结束时,变量 min 包含任何字母的最小计数。我可以用它来提供计数比例,以打印每个字母的相对频率。例如,如果出现次数最少的字母是 q,则 min 将等于 q 计数。

然后我循环遍历每个字母并打印它及其相对频率。我将每个计数除以 min 来打印相对频率,这意味着计数最低的字母将以相对频率 1 打印。如果另一个字母出现的频率是最低计数的两倍,则letter 的相对频率为 2。我在这里只对整数值感兴趣,因此对于我的目的而言,2.1 和 2.9 与 2 相同:

END {
    min = count[1]; for (ltr = 2; ltr <= 26; ltr++) {
        if (count[ltr] < min) {
            min = count[ltr];
        }
    }
 
    for (ltr = 1; ltr <= 26; ltr++) {
        print substr(LETTERS, ltr, 1), int(count[ltr] / min);
    }
}

把它们放在一起

现在我有一个 gawk 脚本,可以计算输入中字母的相对频率:

#!/usr/bin/gawk -f
 
# only count a-z, ignore A-Z and any other characters
 
BEGIN { LETTERS = "abcdefghijklmnopqrstuvwxyz" }
 
{
    len = length($0); for (i = 1; i <= len; i++) {
        c = substr($0, i, 1);
        ltr = index(LETTERS, c);
 
        if (ltr > 0) {
            ++count[ltr];
        }
    }
}
 
# print relative frequency of each letter
    
END {
    min = count[1]; for (ltr = 2; ltr <= 26; ltr++) {
        if (count[ltr] < min) {
            min = count[ltr];
        }
    }
 
    for (ltr = 1; ltr <= 26; ltr++) {
        print substr(LETTERS, ltr, 1), int(count[ltr] / min);
    }
}

我会将其保存到一个名为 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:

$ echo abcdefghijklmnopqrstuvwxyz | gawk -f letter-freq.awk
a 1
b 1
c 1
d 1
e 1
f 1
g 1
h 1
i 1
j 1
k 1
l 1
m 1
n 1
o 1
p 1
q 1
r 1
s 1
t 1
u 1
v 1
w 1
x 1
y 1
z 1

重复该示例,但添加一个额外的字母 e 实例,将打印相对频率为 2 的字母 e,而所有其他字母则为 1:

$ echo abcdeefghijklmnopqrstuvwxyz | gawk -f letter-freq.awk
a 1
b 1
c 1
d 1
e 2
f 1
g 1
h 1
i 1
j 1
k 1
l 1
m 1
n 1
o 1
p 1
q 1
r 1
s 1
t 1
u 1
v 1
w 1
x 1
y 1
z 1

现在我可以迈出一大步了!我将使用 grep 命令和 /usr/share/dict/words 文件来识别所有完全用小写字母拼写的单词的字母频率:

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

在 /usr/share/dict/words 文件中的所有小写单词中,字母 j、q 和 x 出现的频率最低。字母 z 也很少见。毫不奇怪,字母 e 是最常用的。

相关文章: