gawk进行字母加总

字母加总

awk中的关联数组十分灵活方便。这一篇中将会涉及关联数组的用法。
——by hq00e

曾经看过这样的一个签名档:

如果将英语的26个字母由A到Z分别编上1到26的分数,
你的知识(KNOWLEDGE)只能得到11+14+15+23+12+5+4+7+5=96分。
你努力工作(HARDWORK)也只能得到8+1+18+4+23+15+18+11=98分。
只有你的态度(ATTITUDE)才是左右你生命全部的1+20+20+9+20+21+4+5=100分。

想知道还有哪些单词的总和是100吗?这知道答案很容易,只要你会用awk。所以这一次我们要写一个脚本来计算英文单词字母的总和。如果有字典文件还可以找出和为特定值的所有单词和词组。

任务分析

根据前面引用的签名档,字母A到Z将分别由数字1到26来表示——A=1,B=2,…,Z=26。不分大小写所以A=a=1。

  • 首先,要对字母进行编号。方法有很多种,这里我们会使用awk中常用的技巧来完成编号。
  • 其次,读入字母,并根据相应的编号进行加总。

只要两步——看来是个简单任务!现在开始想一想具体怎么用awk实现?或者(如果你对awk不是很熟的话)用其他编程语言怎么实现?

计算总和的脚本

首先,对字母编号。awk中为数组元素分配多个值常用的方法是用split(STRING, ARRAY [, FIELDSEP])函数。它的返回值是数组元互素的个数。需要注意的是:数组的下标是从’1’,而不是从’0’开始的。

1
n=split("ABCDEFGHIJKLMNOPQRSTUVWXYZ",alpha,"")

这条语句会生成alpha数组,内容如下:alpha[1]=”A”;alpha[2]=”B”;…;alpha[26]=”Z”。但我们需要的是以字母为索引找到对应的数值,而不是相反。所以还要进一步加工:

1
while(n) { num_alpha[alpha[n]]=n; n–}

现在得到了一个关联数组num_alpha[]。内容如下:

1
2
3
4
num_alpha["A"]=1
num_alpha["B"]=2

num_alpha["Z"]=3

现在只要以输入的字母为索引就能得到对应的数值了。假设输入”ADD”,只要分别以”A”、”D”、”D”为索引加总:

1
2
3
  num_alpha["A"] + num_alpha["D"] + num_alpha["D"]
= 1 + 4 + 4
= 9

加总的脚本如下:

1
2
3
4
5
6
7
8
9
10
11
{
# 每读入一条记录时,先将总和归0
sum=0
# 将所有栏位的字母加总。非字母字符将被忽略,
# 因为我们并未为非字母字符同值
# 注意:这里用了toupper()。在gawk中还可以设置
# IGNORECASE使之不区分大小写。
for (i=1;i<=NF;i++) sum+=num_alpha[toupper($i)]
# 输出计算结果
print sum
}

上面的脚本中并未对输入进行限定。如果输入含有非字母字符,脚本仅是忽略它们——因为在数组中没有相应的索引,所以不会对运算结果造成影响。但你可能希望对结果进行限定,使得只有当输入中只含字母和空格(词组)时才进行加总。为此我们可以用正则表达式对输入做简单的筛选:/^[a-zA-Z ]+$/(也可以使用字母类[[:alpha:]])。注意后面的空格。
具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
gawk 'BEGIN{
FS=""
# split()的最后一个参数可省略,因为和FS是一样的
n=split("ABCDEFGHIJKLMNOPQRSTUVWXYZ",alpha,"")
while(n) { num_alpha[alpha[n]]=n; n– }
}
/^[a-zA-Z ]+$/{
sum=0
for (i=1;i<=NF;i++) sum+=num_alpha[toupper($i)]
print sum
}'

另一种实现方法

不过我们还可以将这个脚本写得更紧凑一点,我们需要有一种不同的思路。上一个脚本
,我们预先计算每个字母对应的数值,但我们其实可以边加总边计算,即省略了数组这一中间过程。awk提供了一个函数index(IN, FIND),用来返回FIND在’IN’中的位置。而”ABCD…Z”中字母的位置与各自的编号是一致的,因而可以以输入的字母为索引通过 index()来实时计算总和。

1
2
3
4
5
6
7
gawk -vFS="" '{
# 将FS设为空字串,这样每个字母都会被视为一个栏位。
sum=0 # 每读入一条记录时将sum置0
for(i=1;i<=NF;i++) # 将所有栏位相加
sum+=index("ABCDEFGHIJKLMNOPQRSTUVWXYZ",toupper($i))
print sum
}'

下面是一些运行结果:

1
2
$ echo fortune|gawk -vFS="" '{sum=0;for(i=1;i<=NF;i++)sum+=index("ABCDEFGHIJKLMNOPQRSTUVWXYZ",toupper($i));print sum}'
99

财富的确是很重要,看来比爱重要很多——用”love”运行的结果是”54”。不过建议试一下”love and care”——光有爱是不行的还要懂得关怀!

显示特定总和值单词/词组的脚本。

如果你有看上一篇并有找到合适的英汉字典的话,我们还可以让gawk替我们找出所有和为特定值的单词或短语。为此需要增加一条判断语句:

1
2
3
4
5
6
7
gawk -vFS="" '{        # 将FS设为空字串,这样每个字母都会被视为一个栏位。
sum=0 # 每读入一条记录时将sum置0
for(i=1;i<=NF;i++) # 将所有栏位相加
sum+=index("ABCDEFGHIJKLMNOPQRSTUVWXYZ",toupper($i))
# 总和为100的话则输出当前记录($0),当然你也可以改成其他值
if (sum==100) print
}'

当然我们还需要有字典文件,对我们之前用过的字典文件dict.txt2进行加工:
cut -f1 dict.txt >voca.txt
如果用的是Windows的话,你可能没有cut工具,那就用awk:

1
gawk -v FS="t" "{print $1}" dict.txt >voca.txt

运行前面的脚本:

1
gawk -vFS="" '{…省略…}' voca.txt

这是脚本在Windows下的运行结果(看来总和为100的单词和词组还真不少。用我的字典,删除重复项后,共有3177条条目总和为100):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
e:> gawk -vFS="" "{sum=0;for(i=1;i<=NF;i++)sum+=index("ABCDEFGHIJKLMNOPQRSTUVWXYZ",toupper($i));if(sum==100)print}" voca.txt
…省略
American oil
Anchicodium
Anglophobia
Anthozoa
Astralon
AutoMark
Avernus
Bar Draught
Bear steady!
Berlin blue
Bingham body
Bombay hemp
Bouma cycle
Butoxide
C battery cab
Carbolan dye
…省略

当然如果你不想输入那么长的脚本的话,可以将它放到单独的脚本文件中。