碎碎念

2006, April 3

awk实现GB2312向Big5的内码转换

Filed under: awk

Awk读入数据是以记录为单位,每次读入一条记录的数据(记录默认是以“行”为单位,但可以通过设置“RS”来设置记录分隔符),加上支持数字运算、数组和格式输出,很适合用做“格式数据处理”的工具。此外Awk之所以强大正是因为它支持数字运算,这使得它的用途甚至超出了一般的文本处理的范畴。比如我们接下来示范的用awk来实现的GB2312向Big5码的内码转换。……

by hq00e

awk有多个版本,这里所用的awk版本是GNU awk。




Up: (dir)

awk实现GB2312向Big5的内码转换

什么是awk?awk是一种对符合选定的条件(式样)的内容进行操作的编程语言。我们可以通过它对文本内容进行非交互式的编辑。它的语法与C语言相似,但增加了许多文本编辑的特性——尤其最重要的是正则表达式。不过他们还有一大区别就是awk是解释语言,而且解释程序本身相当小,一般在400K以内。只要打开记事本,你就可以编程了。如果你已经会C语言了,那你只需要花一点时间来学正则表达式就可以使用awk了。如果你不大会编程,那我向你保证,awk比C语言简单很多。正则表达式是awk的强力武器,不过我们下面的内容并不涉及正则表达式,也不要求读者会awk——就算不会awk,内容本身也是相当好理解的。

1 准备合适的内码转换表

由于GB2312和Big5码的标准是各自制定的,因而没法通过算法来进行它们之间的内码转换。我们只能通过转换表来实现。应该注意的是通过一对一的转换得到的结果并非完全准确的,因为在简体和繁体中的字符是一对多甚至是多对多对应的。当然我们暂且不考虑那些“深层次”的问题,现在我们将目光集中在如何实现通过转换表进行内码转换。

网上可以找到多个版本的gb-big5表,示例脚本里的用的表是由A1到FF段的gb-big5表,可以到这里下载

如果手头已经有内码转换的软件的话也可以做转换表。可以很容易在gawk来生成gb2312的字符表。

     unix平台: gawk 'BEGIN{for (a=0xa1;a<0xff;a++) for(b=0xa0;b<=0xff;b++) printf "%c%c",a,b }" >gb.TXT
     win平台: gawk "BEGIN{for (a=0xa1;a<0xff;a++) for(b=0xa0;b<=0xff;b++) printf \"%c%c\",a,b }" >gb.TXT

用现有的软件对gb2312表进行转换(有些软件会对结果进行修改导致表不准确),换成Big5的,就获得了一张gb2312-big5的转换表。根据GB2312字符的机器码,进行换算就得到对其在GB2312-Big5表中的位置(但此时该字符为Big5的字符所替代)。将该位置的字符就是该GB2312字符对应的Big5字符。以同样的方式我们可以得到Big5-GB2312的转换表。

现在我们有转换表了,下一步就是在gawk中实现国标码和Big5码之间的相互转换。假设我们要实现gb-big5的转换,那要怎么开始呢?




Next: ,
Up: Top

2 在gawk中读入转换表

首先应该读入转换表,我们可以将转换表中的字依序存放在数组中。我们只要将输入字符的机器码进行换算再作为数组的下标就可以得到相对应的big5字符了。其实也可以不先读入转换表,而是每次转换时打开文件根据机器码换算在文件中的偏移量。但在awk中读打开关闭文件的操作相当耗时——所以我们还是采用预先读入表到数组中。

二是字符在表中的位置是与机器码相对应的,因此我们要得到输入字符的机器码(即字符的十六进制值)。awk没有提供内建类似ord()的函数。但我们可以手工添加(注:在gawk的文档中也有一段类似的内容。在gawk文档查找一下“`chr'”“ord”看有什么收获!)。添加的方法,我们在后面在探讨。现在先看一下读入转换表的函数怎么写:

1 func getTable( i,k){
2  if ((getline < "gb2big5.tab")>0 )
3  for (i=1;i<=NF;i++) {
4   tab[++k]=$i $(i+1)
5   i++
6  }      #每次读入两个字节
7 }

第1行中我们自己定义了一个函数:getTable ,有三个参数。我们会在awk脚本的`BEGIN'段中,通过以下方式调用这个函数以初始化转换数组。

     BEGIN{ getTable(tab) }

实际上我们在调用这个函数时只用了一个参数“tab”,但在定义函数时我们却定义了三个参数。这是因为awk在参数列表中指定本地(局部)变量,通过在参数表中指定变量,我们将i和k声明为本地变量。

第2行的代码从“gb2big5.tab”中读取一行数据——实际上这个文件也只有一行(很长的一行)。

第3-5行的代码将读入的数据(转换表),以每两个字节(相当于一个汉字)为单位存放在相应序号的数组里。这样转换表中的前两个字节(也就是第一个Big5码字),将放在tab[1]中。第三、四个字节(也就是转换表中的第二个Big5码字),将放在tab[2]中。




Next: ,
Previous: chap1,
Up: Top

3 在gawk中获取字符的机器码

下一步,我们办法获得输入字符的机器码。比如,在GB2312中“阿”的机器码是十六进制的“b0 a2”。在当读入“阿”时,我们要有一个函数ord("阿")的时候能返回“b0 a2”。不过因为国码码是两字节的编码,我们可以分两次处理使用ord()分别来得到“阿”高位和低位的机器码。GB2312中汉字是高低字节都是从A0开始到FF结束的,所以我们反其道而行——先通过机器码杖举对应的字符,将结果用数组存起来。这样我们就可以建立一个机器码与字符相对应的表了。这样我们就可以通过字符来查找机器码了。在awk中可以用下面的代码实现:

     func _ord_init( i ) {
      for (i=0xA0;i<=0xFF;i++) ord[sprintf("%c",i)]=i
     }

有没有注意到awk的代码相当紧凑呢?sprintf是awk内建的函数,用c语言的人应该相当熟悉了。sprintf将数值转换为相应的字符(%c),并返回该字符。ord则将该字符作为数组的下标,所存的值正是用做下标的符的机器码。我们只要以输入字符作为数组的索引就能得到相应的机器码了。

提示:awk将下标都视作字符,因而对awk来说a[1]和a["1"]是一样的。事实上awk中数字和字符的介定相当模糊,数字字串经常可以不经显示转换地当成数值使用。看一下下面的例子:

     gawk 'BEGIN{str="123"; print str+3}'       输出:126
     gawk 'BEGIN{str="a123"; print str+3}'       输出:3

注意第二个字串中含有非数字字符,被当成0对待。另外在awk中下标可以是任意长度的任意字符,这给我们很大的灵活性——你可能会发现这与perl中的hash表的概念很相近。




Next: ,
Previous: chap2,
Up: Top

4 用机器码计算字符在表中的位置

现在我们已经读入了转换表,也知道怎样获取字符对应的机器码了。接下我们要知道怎样将gb2312编码汉字的机器码换算为转换表中相应的字符的位置。换算公式很简单,我们以“h_b”表示高位的机器码,“l_b”表示低位的机器码:

     (h_b-161)*96+l_b-160+1

“阿”高位是0xb0,低位是0xa2。分别是十进制的176和162,代入上面的公式:

     (176-161)*96+162-160+1=1443

而数组ord[1443]的值就是我们想要的那个Big5的“阿”了。




Next: ,
Previous: chap3,
Up: Top

5 完整的转换脚本

现在,我们只要将上面的内容结合起来就很容易用awk写出一个将gb码转换为big5码的程序了:

############ 开始 ##########
#!/usr/bin/gawk -f
# filename: gb2big5.awk
# gb2312码转big5码程序
	
BEGIN{ FS=OFS="" ; getTable(); _ord_init()}
	
func getTable( i,k){
if ((getline < "gb2big5.tab")>0 )
 for (i=1;i<=NF;i++) {tab[++k]=$i $(i+1); i++}
}
	
func _ord_init( i ) {
 for (i=0x40;i<=0xFF;i++) ord[sprintf("%c",i)]=i
}
	
{
 for(i=1;i<=NF;i++) {
  h_b=ord[$i]
  if (h_b >= 161) {
     l_b=ord[$(i+1)]
     $i=tab[(h_b-161)*96+l_b-160+1]
     $(i+1)=""
     i++
   }
  }
print
}
############ 结束 ##########

连空行,我们只用了24行就写了一个内码转换(GB2312 -> Big5)的脚本。这个脚本相当容易理解,如果还有需要解释的地方的话就是FS和OFS了。在BEGIN段中的:

     FS=OFS=""

通过将栏位分隔符(Field Separator即FS)设为空字串,使用awk将每个字节视为一个Field。这样我们就可以通过for语句逐个字节地读入数据,下面的语句将当前Record中的数据逐字节地存在数组arr中:

     for (i=1;i<=NF;i++) { arr[i]=$i }




Previous: chap4,
Up: Top

6 运行结果

现在我们看一下运行结果:

hq00e@somewhere ~
$ cat aa.txt
简体中文测试abcd
	
hq00e@somewhere ~
$ gawk -f gb2big5.awk aa.txt
簡體中文測試abcd
	

以同样的方式我们还可以很容易地写出将Big5转为GB2312码的脚本。大家不防自已动手试试。

2006, March 30

将阿拉伯数字转化为中文大写形式的AWK脚本

Filed under: awk

将阿拉伯数字转化为中文大写形式的AWK脚本

原来在w0rdpress上帖过的,本来想扩充成一篇教程再帖在这里的。但打了几个字就决定放弃了,实在是累啊……这里面用到了自定义的函数,大家可以参考一下。还有欢迎大家来信交流hq00e@126.com。

hq00e

这是一个将阿拉伯数字转化为相应的中文大写形式的gawk脚本。前几天有人问起能否用awk实现数字转中文大写,于是写了这个脚本。本来想是挺简单的,没想到修修补补的还折腾了一晚 :(

这里面用到了两个数组a和p,刚接触awk的人可以参考一下这里面数组的用法。个人认为使用数组可以大大地简化脚本。尤其是awk中对变量的类型没有严格限定,可以很容易地在数组的索引与值之间建立关联。
下面是脚本:

#!/usr/bin/gawk -f
# 1234567890 壹贰叁肆伍陆柒捌玖零
# 说明:单位以“亿亿”表示10的16次方,而“亿亿亿”表示
# 10的24次方,依此类推
	
# 自定义函数setunits():为数字增加中文单位
# setunits(起始位,结束位,亿的个数, ...局部变量)
# 一般以8 个为一个单位
func setunits(off1,off2,state,   i,j){
	for(i=off2;i>=off1;i--) {
		if (off2-i==4) $i=$i "万"
		else if($i!="0") $i=$i p[(off2-i)%4]
	   }
	# 每8位state就增加1,用来控制单位中“亿”的个数。
	j=state; while(j--) $off2=$off2 "亿"
	# 退出条件
	if (off1==1) return
	else if (off1<=9) setunits(1,off1-1,++state)
	else setunits(off1-8,off1-1,++state)
}
	
# 自定义函数conv_num():将数字转换为中文
# 定义局部变量i
func conv_num(  i){
	# 注意下面替换的顺序
	sub(/0*\./,"点")
	# 清除连续的0
	gsub(/0000万0000[亿]+/,"")
	gsub(/0000万/,"")
	gsub(/0+亿/,"亿")
	gsub(/0+万/,"万")
	gsub(/0+/,"0")
	sub(/^[亿万]+/,"")
	#清除首尾的0
	sub(/^00*/,"")
	sub(/00*$/,"")
	sub(/^$/,"零")
        sub(/^点/,"零点")
	# 替换阿拉伯数字为中文
        for(i in a) gsub(i-1,a[i])
}
	
# 主程序体 -------------------------
# 设置栏位的输入和输出的分隔符为空字串
BEGIN{  FS=OFS=""
	split("拾,佰,仟,万",p,",")
	split("零,壹,贰,叁,肆,伍,陆,柒,捌,玖",a,",")
     }
	
{ gsub(/,/,"") } #允许输入 123,456.789 的形式
	
# 验证输入。这里没有对多个小数点进行验证。
/^[-0-9][0-9.]*$/{ 
	
	# 判断正负
	isNeg=(sub(/^-/,""))
	
	# 小数点前的数字进行单位设置
	if (dotpos=index($0,".")) {
		of2=dotpos-1
		for (i=of2+2;i<=NF;i++) $i=a[$i+1]
	}else of2=NF
	if (of2>8) setunits(of2-7,of2,0)
	else setunits(1,of2,0)
	
	# 阿拉伯数字转为中文数字
	conv_num()
	
	# 还原正负号
	if (isNeg && $0!="零") print "负" $0
	else print
	}
	
# 程序结束 -------------------------

将上面的脚本保存为”chfig.awk”(当然你可以随意地命名)。这是运行结果:

hq00e@somewhere ~
$ echo 98.76543210|gawk -f chfig.awk
玖拾捌点柒陆伍肆叁贰壹零
	
hq00e@somewhere ~
$ echo -10000234000.060|gawk -f chfig.awk
负壹佰亿零贰拾叁万肆仟点零陆零
	

[–结束–]

字母加总的AWK脚本

Filed under: awk




Up: (dir)

字母加总

awk中的关联数组十分灵活方便。这一篇中将会涉及关联数组的用法。同时还示范了将FS设为空的用法:-0

–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。所以这一次我们要写一个脚本来计算英文单词字母的总和。如果有字典文件还可以找出和为特定值的所有单词和词组。





Next: ,
Up: Top

任务分析

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

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

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





Next: ,
Previous: 任务分析,
Up: Top

计算总和的脚本

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

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

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

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

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

num_alpha["A"]=1
num_alpha["B"]=2
...
num_alpha["Z"]=3

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

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

加总的脚本如下:

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

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

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()‘来实时计算总和。

gawk -F '' '{
   sum=0               # 每读入一条记录时将sum置0
   for(i=1;i<=NF;i++)  # 将所有栏位相加
     sum+=index("ABCDEFGHIJKLMNOPQRSTUVWXYZ",toupper($i))
   print sum
 }'

提示:命令行选项-FFS设置为空字串,这样每个字母刚好是作为一栏。这与在脚本的BEGIN段使用`FS=""‘语句是一样的。-F ''也可以写成-v FS=''的形式。下面是一些运行结果:

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

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





Previous: 另一种实现方法,
Up: Top

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

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

gawk -F '' '{
   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:

gawk -F "\t" "{print $1}" dict.txt >voca.txt

确保字生成的字典文件与脚本在同一目录中,然后运行前面的脚本就大功告成了。

gawk -F '' '{...省略...}' voca.txt

这是脚本在Windows下的运行结果3

e:\>gawk -F "" "{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
...省略

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

[–结束–]

Footnotes

[1] 也可以使用字母类[[:alpha:]]
[2] 见“gawk显示指定内容”
[3] 看来总和为100的单词和词组还真不少。用我的字典,删除重复项后,共有3177条条目总和为100

AWK做字典查找程序

Filed under: awk




Up: (dir)

AWK做字典查找程序

输出满足“特定条件”(通常是匹配式样)的内容是awk最基本的功能。上一篇中我们将这个“特定条件”(由随机数指定的记录数)放在awk脚本中,让awk每次随机输出记录。这一篇中我们将会演示用传递变量的方式在每次运行脚本时由用户指定这一“特定条件”。
by hq00e

上一篇中我们已经完成了一个用gawk实现的用来随机读取表情符号的shell脚本readsmiley。这一篇中我们将示范如何用gawk显示符合特定条件的内容。同时会继续完善上一个脚本,使之可接收参数。像这样:

$./smiley '8:]'
8:]             From a gorilla
                Gorilla

而在不使用参数时,该脚本仍像上一篇中那样随机地显示表情符号。

不过在那之前我们会先讲解一个简单的例子——gawk的查字典脚本。

背景知识

正则表达式与字串。在awk中正则表达式是`/regexp/‘的形式出现的——正则表达式的两端是`/‘。而字串则是以`"string"‘的形式出现——两端是`"‘。但字串也可以用作正则表达式,所不同的是在`"‘号中使用正则表达式需要使用转义符。

在awk中比较字串可以使用==~,两者的不同表现在两个地方:

  • ~将等式右边的值视为正则表达式,等式左边只能使用字串。==将等式两边都视为字串。
  • 使用前一个逻辑运算符时,两边只有完成一致才返回真值。使用~时,只要运算符左边的字串含有左边字串就为真。

以字串表正则时转义符的使用。在字串形式的正则表达式的规则与一般的正则表达式稍有不同,如:与正则表达式`/\/file\.ext/‘等价的字串形式的正则表达式是`"\\/file\\.ext"‘。考虑下面的例子:

hq00e@somewhere ~
$ gawk 'BEGIN{if("/file.ext" ~ "\\/file\\.ext")print "true"}'
true
	
hq00e@somewhere ~
$ gawk 'BEGIN{if("/file.ext" ~ "/file.ext")print "true"}'
	
hq00e@somewhere ~
$ gawk 'BEGIN{if("/file.ext" == "/file.ext")print "true"}'
true
	

第一、二个例子中,使用了`~‘运算符,等式右边的内容被视为是正则表达式。而最后一个例子中使用了`==‘,两边的内容都是字串。至于在字串中使用正则表达式的具体内容可参阅相关的文档,不再赘述。

gawk接收外部变量的方法是使用命令行参数-v。比如要在脚本外部定义一个变量`myword‘,并在脚本中引用可用下面的命令:

$ gawk -v myword="hello" 'BEGIN{print myword}'

`-v‘与`myword‘中的空格也可以省略。将用的技巧是通过`-v‘选项在脚本外部定义`RS‘和`FS1

$ gawk -vRS="" ...

上面的命令在脚本外部定义了`RS‘为连续的空行。在命令行中使用时这样可以减少一些输入——在脚本中输入则为`BEGIN{RS=""}‘。在Windows的命令中使用时这样可以减少转义字符`\‘的使用。





Next: ,
Previous: 背景知识

写一个查字典脚本

现在开始构造一个查字典的脚本。这个脚本完成的所功能相当简单,我们可以很容易地用其他Unix工具完成同样的任务。但这个脚本所用到的技巧,将为以后写更复杂的gawk脚本奠定基础。

找到合适的字典文件

首先要找到合适的字典。我手头上的字典是以前在汉化新世经网站找到的。像下面的这种的格式:

……
hellfire	地狱之火
hellgramite	翅虫的幼虫
hellgrammite	翅虫的幼虫
hellhole	不舒服场所
hellhound	地狱之犬
hellion	坏人
hellish	地狱般的
hellkite	残忍的人
hello	喂
helluva	很难的
hellward	向地狱
helm	舵
……

前面是英文单词后面是对应的中文解释。中英文之间用制表符隔开。每行对应一个单词或短语。

如果你找到的字典是其他格式的话,我们可以通过sedgawk,等命令来转换,或者修改gawk脚本中的`RS‘。我手头上有一本英西字典的格式如下:

August [蓴藧g蓹st]
   agosto
	
Australian [蓴streili蓹n]
   australiano
	
Austrian [蓴stri蓹n]
   austriaco
	

可以看到记录与记录之间是用空行隔开的而不是我们想要的以制表符隔开。但是可以使用gawk脚本,转换为我们想要的格式:

hq00e@somewhere ~
$ gawk 'BEGIN{RS=""; FS="\n"}{print $1 "\t" $2}' eng-spa.dict
...
August [蓴藧g蓹st]	agosto
Australian [蓴streili蓹n]	australiano
Austrian [蓴stri蓹n]	austriaco
...

但是更好的办法应该是将`RS‘设为`""‘!

字典脚本

现在我们要查找单词`tunami‘。使用如下命令:gawk '/tunami/{print $0}' dict.txt现在我们知道这条命令也可以写为gawk '/tunami/' dict.txt。但我们需要让它能接收外部变量这样我们就能在命令行使用。gawk -vtarget="$1" '$0~target' dict.txt。我们输入的文字是被当作正则表达式使用的,但这样使用相当麻烦就算我们想真想用正则表达式,看一下这个例子:
gawk -vtarget="B\\.C" '$0~target' dict.txt
(上面的脚本只要给的参数是中文就能查找对应的英文了)
我们在`.‘前用了两个反斜杠使gawk将其当成一个普通的句号。不想使用正则表达式的话将`$0~target‘改为`$1==target‘。

现在我们有一个能使用正则表式的的字典查找脚本了。慢,这不是用grep就可以做到了吗?`grep tunami dict.txt‘。……的确是的……,汗。不过将上面的脚本进行扩充,我们还可以做一个交互式的词典!下面是脚本的内容:

#!/usr/bin/gawk -f
# ----------------------------------------开始
# 从CL读单词,然后从"dict.txt"找单词
# 在每次进行查找时,我们使用getline来重新读入字典。这种方式速度
# 较慢,但对内存要求没那么高。另一种实现的方式是通过关联数组。以
# 关联数组实现的话速度快,但使用的内存比较多(具体的量与字典的大
# 小有关)。
# 使用方式:`gawk -f lookup.awk'              #不需要参数
	
BEGIN{FS="\t" ; RS="\n"
	printf "输入要查找的单词(输入“qquit”退出):"
}
	
{
	# 读取用户输入
	word_to_find=tolower($1)
	# 退出条件
	if(word_to_find~/^qquit$/)exit
	# 输入空格或没输入的话,不进行进一步处理
	if(word_to_find!~/^[ \t]*$/){
		# 去除一小部分字元避免不便。
		# 保留常用字元如:^.*+$
	        gsub(/[-[&]/,"\\\\&",word_to_find)
		# 读入字典文件
		while((getline<"dict.txt")>0){
			# 此处的$1与先前的$1不同。前一为命
			# 令的$1,现在这个$1是"dict.txt"的$1。
			# 不区分大小写。
			if (tolower($1)~word_to_find){
				print $0
			}
		}
		# 关闭字典文件
		close("dict.txt")
	}
	
	printf "\n输入要查找的单词(输入“qquit”退出):"
}
# ----------------------------------------结束

这个字典引擎支持部分正则表达式字元,不区分大小写。下面是使用示例:

$ ./lookup.awk
输入要查找的单词(输入“qquit”退出):tunami
tunami  海啸
	
输入要查找的单词(输入“qquit”退出):^CrE..t$
credit  贷方
credit  贷记;信贷
credit  信用;贷款;贷方;债权
	
输入要查找的单词(输入“qquit”退出):qquit
	

打造全功能的`smiley‘命令

我们接下来看一下如何为我们上一次写的readsmiley脚本增加功能使之能接收命令行参数。你应该已经知道了它的实现原理与上面的字典脚本将会是一样的。我们将通过gawk-v选项向gawk传送变量参数。废话不多说先看脚本:

#!/usr/bin/sh
gawk -v sface="$1" 'BEGIN{
    RS=""
    FS="\t"
 }
 # 不需要用到正则表达式,所以下
 # 面的比较用了“==”运算符。
 $1==sface' mysmiley

将上面的脚本命名为readgivensmiley,修改执行权限,就可以执行了。这个脚本要求一个参数。下面是运行的结果:

$ ./readgivensmiley ":-*"
:-*             Kiss...
                Oooops (covering mouth with hand)
                User just ate a sour pickle
                User just ate something sour
                Smiley after eating something bitter
                After eating something bitter or sour
                Star-tled
                Blowing a kiss
	

需要注意的是,shell脚本第二行中的`$1‘与最后一行中的`$1‘是不一样的。前者表示的是bash shell的第一个参数——即上面运行示例中的`:-*‘。这个参数在传给变量`sface‘之前会被展开为`:-*‘(注意:`$1‘两边用了双引号`"‘,确保shell能将它当作一个命令行参数变量并展开为变量所表示的值。)而后者是在gawk的脚本之中,表示的是记录中的第一栏(即表情符号的部分)。另外我们比较字串用的运算符是`==‘这要求用户给的参数要与表情完全一样才能得到某个表情的相关解读。

现在我们有个readsmiley脚本和readgivensmiley分别用来随机显示表情符号和显示指定的表情符号。但要更好的模拟smiley命令我们需要将两者功能合而为一。为此,我们的最终脚本需要判断用户有没有提供命令行变量的参数,这一步可以通过shell来完成——参数为空执行readsmiley否则执行readgivensmiley,这应该是相当容易实现的。不过也许你不想在一个shell脚本中使用两个gawk脚本,下面的脚本演示了如何将上面的两个脚本合二为一。为了使之容易理解我将尽量保留原有两个文件的结构和内容:

#!/usr/bin/sh
gawk -vsface="$1" 'BEGIN{
     RS=""
     FS="\t"
  # 当“sface”为null时,则“!sface”为true。
   if (sface==""){
      RS="\0"
      FS="\n[ \t]*\n"
      srand()
      }
    }
    !sface { print $int(NF*rand()+1); exit 0}
    $1==sface ' mysmiley

将上面的命名为smiley——现在你自己的系统中也有smiley命令了。当你未加参数时,它将随机显示表情符号及它的各种解读。当你使用参数时它将找出该参数对应的各种解读。下面是运行的结果:

hq00e@somewhere ~
$ ./smiley
( :oF           Bill Clinton enjoying some French fries
	
hq00e@somewhere ~
$ ./smiley "8-*"
8-*             Just ate a hot pepper





Previous: 打造smiley命令

附:在Windows的批处理中使用gawk脚本

在Windows的批处理中使用gawk脚本会麻烦些。因为在无法在命令行中跨行使用脚本,同时脚本两端使用的单引号要用双引号替找,而脚本中所使用的双引号之前都要加上转义字符`\‘。像这样的脚本:

gawk 'BEGIN{
       var="hello!"
       print var }'

在Windows的命令行下应该写为:

gawk "BEGIN{ var=\"hello!\"; print var }"

对于短一点的脚本影响不太,但对长一点的脚本而言就比较麻烦。为了避免这些麻烦,通常的做法是将脚本的部分放到独立的文件中。

在smiley的例子中我们假设脚本被放到名为as.awk中。smiley脚本对应的批处理文件是:

@echo off
rem 脚本被存放在as.awk中
gawk -vsface="%1" -f as.awk mysmiley

将上面的内容保存为smiley.batas.awk放在同一个目录中。使用示例:

e:\>smiley
>;<             Butterfly
	
e:\>smiley "3:o"
3:o             Smiley calf
	

[–结束–]




Footnotes

[1] gawk中提供了选项-F在脚本外部定义`FS‘,但FS只接受一个字符的参数。为了在脚本外部定义更复杂点的正则表达式为`FS‘,我们可以用:`gawk -vFS="…"‘的方式定义`FS

2006, March 29

awk随机读取smiley

Filed under: awk
  • awk随机读取文件内容
    • 背景知识
    • 开始动工
    • 改良脚本
    • 最终方案
    • 基于这个例子的一些扩展应用
    • 下一篇

awk随机读取文件内容

这篇文章要求读者对正则表达式有基本的了解,同时对awk的编程模型有个概念——就是awk的工作流程。有任何批评建议都可以联系我,我的电子信箱是“hq00e @ 126 点 com”。

记得在Unix系统中有一个“smiley”命令用来随机读取表情符号。当你在shell提示符下输入`smiley‘就会得到如下的输出:

     $ smiley
     :,-D            User is laughing so much that they're crying

不过并不是每个系统都能找到这个命令。现在我们要写一个与smiley功能一样的脚本来演示GNU awk1的用法——你会发现用gawk完成这种任务竟是如此轻松!

什么是smiley faces?smiley faces就是表情符号如我们经常见的`: )‘就是一个smiley face。当然类似的符号还有很多,比如:`:-(‘或`8-O‘。你可以到这里找到更多smiley。将你自已收集的表情符号——没有的话就从前的提供的网址中复制吧,然后保存为文件“mysmiley”。我们将写一个gawk脚本来随机的读取mysmiley文件中的某一个表情符号及其对应的解释。现在你的“mysmiley”应该是像这样,一个表情符号后可能带有多于一行的解释。不同的表情符号间用一空行分隔。

:^)             From a person with a large nose
		Messages teasing people about their noses
		3/4 view of person with elf-type sharp nose
		Broken nose
		Happy
	
:-@{#@}           From a person with braces
	
@:-)            From a person with wavy hair
		User is wearing a turban
		Smiley wearing a turban
		Indian (East/Asian)
		Elvis
		A turban
	
O:-)            Angel
……

背景知识

gawk读入数据是以记录为单位,一次读入一条。哪怎样算是一条记录呢?gawk用了一个内置的变量来设定记录与记录之间的分隔符,这个变量是RS——注意大小写。这个变量的默认值是一新行符,即如果没有指定RS的值gawk将以行为单位一次读入一行。而每一条记录又可以分为多个栏位。同样的gawk有一个内置的变量来设置栏位的分隔符——FS。这个变量的默认值是空格/制表符。

与RS和FS相关的还有两个变量分别是NR和NF分别表示当前记录在文件中的序号和当前记录的栏数2。NR是从1开始计算的。举例来说,假设某文件的第二行内容如下:

     aa bb cc	dd

则在没有改动RS和FS的情况下,当gawk读入该行时NR的值为2,NF的值为4。可以在gawk中用“$1”、“$2”、“$3”、“$4”来分别引用“aa”、“bb”、“cc”、“dd”。而用“$0”来引用“`aa bb cc dd‘”。更具体的说明见相关的手册页。

gawk提供了一个rand()函数来产生随机数序列和一个srand()函数来产生随机序列种子。记得在gawk脚本的BEGIN段使用srand()函数,这样每次使用rand()函数产生的随机数序列才会不相同。如果不明白什么是随机序列种子的话,可以先记住在rand()函数前使用srand()会让随机数“更像”随机数。

开始动工

首先要确定mysmiley文件中的条目之间是如何分隔的。我们可以看到这些表情符号后面及下面紧邻的行都属于同一条目。这一条目的范围一直到遇到空行,而空行下是另一表情符号。可以看到条目与条目之间是用空行分隔的,所以我们可以将RS3设为空行:` RS="\n\n" ‘ 。然后设置一个随机数来随机读取某条记录,下面的命令将产生一个0到400之间的随机整数:`int(400*rand())‘。剩下的就相当简单了,我们只要结合起来:

     gawk 'BEGIN{RS="\n\n";srand();recno=int(400*rand())}recno==NR{print $0}' mysmiley

……是不是比你想像中要容易呢?不过还没结束呢。对于上面的脚本还要再补充一点:我们已经产生了一个0-400之间4的随机数并存放在变量recno中。我们要将第recno条记录显示出来,即当当前的记录数NR与recno相等时打印记录:`recno==NR{print $0}‘使用print时,没有提供参数默认是打印$0,所以上面的命令可以省略为:`recno==NR{print}

技巧:有一个特殊的情况是当RS又被设定为空字串:`RS=""‘时,记录的分隔符成为了连续的一个或以上的多个空行。不过这一技巧对于FS并不适用。另外在gawk中当条件为真时如果没有提供处理语句,默认是打印当前记录。所以recno==NR也与recno==NR{print $0}是一样的。现在我们的命令可以写成:

     gawk 'BEGIN{RS="";srand();recno=int(400*rand())}recno==NR' mysmiley

我们可以随机的读取0-400条记录了。但还有几个问题:

  • 首先,没有第0条记录。
  • 其次,mysmiley文件中可能有不只或不到400条记录。

我们再看看怎么解决这些问题?

改良脚本

理想的随机数的范围应在1到总的记录数之间。看来我们得先得到总的记录数。通过shell有多种方法可以做到这一点,但我们还是用gawk:

     gawk -vRS="" 'END{print NR}' mysmiley

这条命令打印最一一条记录的序号。也就是mysmiley文件中的记录数。下面是完整的shell脚本:

     #!usr/bin/sh
     totalRecords = `gawk -vRS="" 'END{print NR}' mysmiley`
     gawk -vtr="$totalRecords" '
      BEGIN{
        RS=""
        srand()
        recno=int(tr*rand()+1)
       }
      recno==NR' mysmiley

不过我们还是寻求更纯粹的gawk方案。在上面的方法中我们通过在shell使用一条gawk命令来得到mysmiley的记录数,有没有办法不使用shell而直接在gawk得到mysmiley文件的记录数并显示相应的记录呢?照原来的思路看来是不行的,因为当我们得到记录总数——最后一条记录的NR时程序已经执行快完了5。解决的办法是将每一条记录以记录的序号为下标存在数组中,然后在END段中(此时我们已得到了总的记录数),产生一个随机数。以这个随机数做为数组的下标读取数组。但我们将不示范这种方法,读者可以自已试一试这种使用数组的方法。这里要使用的是另一种方法。

最终方案

gawk一次读入一条记录所以所以得等到读入最后一条记录时才能得到总的记录数。而栏位数是一读入一条记录就得到了总的栏位数即NF。如果我们将mysmiley整个文件视作一条记录6而将里面的每一条目视为一栏,那我们就可以通过NF得到总的条目数并随机地显示某栏(即条目)的内容。显然我们只需要重新地设定RS和FS就完成了一半了。

为了将整个文件当成一条记录,可以将RS设置为文件中不存在的字串。如果这个mysmiley文件的内容是都是英文的我们可以设置RS="你好吗?",或是随便什么中文句子,来“强迫”gawk一次读入整个文件。但比较常用的是使用“\0”7,来达到这一目的。至于FS通过观察我们可以看到上面的mysmiley文件中不同的条目间有一空行,所以我们可以用“\n\n”分隔栏,但因空行中可能有空白字行如制表符或空格符所以我们将之改为“\n[ \t]*\n”。下面是成品:

     #!usr/bin/sh
     gawk 'BEGIN{
            RS="\0"
            FS="\n[ \t]*\n"
            srand()
           }
          { print $int(NF*rand()+1) }' mysmiley

最后再解释一下“$int(NF*rand()+1)”。先是NF*rand(),将栏位数乘以一个0到1之间的随机数,得到了一个大于等于0小于NF的随机数。再加上“1”我们得到了一个大于等于1小于NF+1的随机数。用int()函数对这个数取整,我们得到了大于等于1小于等于NF的随机数。我们在前面加上“$”号得到相应栏位的值。假设“int(NF*rand()+1)”得到的值是“3”,那“$int(NF*rand()+1)”就等同于“$3”。而“print $3”将把第三栏的内容,即mysmiley文件中的第三条表情符号及相应的解释,显示出来。

现在我们知道脚本运行的原理了。将脚本保存为“readsmiley”,并与mysmiley文件放到同一目录中。为readsmiley增加执行权限,就可以运行了:

     ./readsmiley

如果你用的是Windows的话,先确保gawk在搜索路径中然后将脚本改成单行的形式:

     gawk "BEGIN{RS=\"\0\";FS=\"\n[ \t]*\n\";srand()}{print $int(NF*rand()+1)}" mysmiley

保存为readsmiley.bat并与mysmiley文件放在同一目录中。运行readsmiley.bat就行了。如果你用双击的方式运行这个批处理文件你可能会发现一个黑色窗口一闪而过。让窗口留片刻的方法是在这人批处理文件的末尾加上`pause‘命令8

基于这个例子的一些扩展应用

这个例子主要演示了FS和NS的用法。我们完全可以将之扩展到其他方面的应用。比如随机显示通讯录中的某条记录;显示某条笑话;随机地显示英文单词及它的中文解释等等。通常情况下我们只要相应地修改FS和NS就能应用于这些方面了。

我们以Best of Vim Tips为例,看如何改脚本来适应新的需要。Vim Tips文件中有许多有用的Vim使用技巧,通过修改脚本我们让它每次运行时显示一条技巧。先看一下该文件的结构:

" Combining g// with normal mode commands
:g/|/norm 2f|r*                      : replace 2nd | with a star
"send output of previous global command to a new window
:nmap <F3>  :redir @@a<CR>:g//<CR>:redir END<CR>:new<CR>:put! a<CR><CR>
----------------------------------------
" Global combined with substitute (power editing)
:'a,'bg/fred/s/joe/susan/gic :  can use memory to extend matching
:g/fred/,/joe/s/fred/joe/gic :  non-line based (ultra)
----------------------------------------
" Find fred before beginning search for joe
:/fred/;/joe/-2,/sid/+3s/sally/alley/gIC
----------------------------------------
" create a new file for each line of file eg 1.txt,2.txt,3,txt etc
:g/^/exe ".w ".line(".").".txt"
----------------------------------------
……

可以见到每条tips都是以“" ”开始的,将FS改为“\n\"”就行了:

     gawk 'BEGIN{
            RS="\0"
            FS="\n\""
            srand()
           }
           {print "\"" $int(NF*rand()+1)}' vimtips

现在我们只要运行这个脚本就可以随机显示一条Vim提示了。你可以在系统中设置让这条命令在登录时自动运行,或是做成shell的提示符。

你还以用同样的技巧做许多的事。当然,还能怎么用就看你的创意了。

下一篇

使用过smiley命令的人应该知道,这条命令还有带参数的用法。如输入`smiley ':-)'‘会得到对表情符号`:-)‘的相关解释。我会在下一篇中说一说在gawk中使用参数的方法。同时我们还将一起写一个能查字典的脚本。下一篇再见……

PS:在以后的文章中,内容会逐渐涉及gawk的各个方面。我们近期可能会讲到的有getline、gawk中的管道、gensub、一些内置变量如FILENAME等。有兴趣的读者不防先看一看这方面的有关资料。另外包括awk、sed在内的许多Unxi工具的使用都相当依赖于用户对正则表达式的掌握程度,所以要使用awk或其他Unix工具(许多Unix工具也有Windows版本),一定要有意识地学习和掌握正则表达式的用法。


Footnotes

[1] 虽然是用gawk但这里面的一些方法并非只有gawk能使用。但因为可能使用到一些GNU扩展,而我不想时刻地注意着哪些是GNU扩展哪些不是。所以下面一概以Gawk代替awk——即使有些并没有用到GNU扩展。
[2] 准确地说NR表示的是在已读入的所有记录中的序号。对应的还有一个FNR表示的是当前记录在当前文件中的序号。如果只有一个输入文件则这两个变量的值是一样的。在本文中将只考虑一个输入文件的情况。
[3] RS是记录分隔符,设置RS可以让gawk知道我们是以什么为标准来分隔不同的记录的
[4] 不包括400
[5] 这与gawk的运行模式有关,gawk逐条读入记录并且读入一条记录时上面的一条记录就被舍弃。当gawk读到第五条记录时你没办法让它再对第三条记录进行操作。除非你将先前读入的记录存在数组或变量中
[6] 注意:将整篇个文件当成一条记录读入,会占用较多的内存。所以如果系统资源紧张或者读入的文件太大的话则不适合用这种方式。
[7] 在一些其他版本的awk中可能没法使用这一方法。另外字过c语言的应该都会知道“\0”就是用来表示字串结束的数字0。
[8] 如果你用的是Win2000以上版本,你也可以使用管道让随机读取的条目显示在消息框中。方法是:`gawk "BEGIN{RS=\"\0\";FS=\"\n[ \t]*\n\";srand()}{print $int(NF*rand()+1)}" mysmiley | msg %username%‘。

Get free blog up and running in minutes with Blogsome
Theme designed by Jay of onefinejay.com