Awk实例 第二部分
起因:近日google一把awk的教程,翻到IBM developerworks上一个初级的awk系列文章,是Gentoo Linux创始人Daniel Robbins在多年前写的。细看一下觉得很不错,可developerworks中国上面的中文版却是缺斤少量,比起原文整段整段的丢失,让人看得糊涂。不知是机器翻译问题还是版面问题?于是只有去翻看英文版(本人英语那个差啊,逼着看),遂有重新翻译一遍,权当为自己保留,也为其他有需要的朋友保留吧。尽管是初级内容,还是很值得一看,确实没想到Gentoo的创始人会写这么基础的东西来普渡众生。
原文地址: http://www.ibm.com/developerworks/linux/library/l-awk2/
Common threads: Awk by example, Part 2
记录、循环和数组
概要:在这篇介绍awk的后续文章中,Daniel Robbins 将带领我们继续浏览这个名字怪怪的伟大语言。 Daniel将向你展示如何处理多行记录,使用循环结构,创建和使用数组。完成本文后,你将能熟练awk的大部分特性,并将有能力写出你自己的强大awk脚本。
多行记录
AWK是处理类似/etc/passwd这类结构化数据的的优秀工具。/etc/passwd是UNIX的用户数据库,一个用冒号分隔的文本文件,包含大量的诸如用户账号、用户ID等重要信息。上一篇文章中我已经演示了awk是如何轻易解析这个文件的,awk解析这个文件时我们必须做的就是把FS变量设置为“:”。
通过正确的设置FS变量,awk几乎能解析任何类别的此种每行代表一个记录的结构化数据。然而,如果我们想要解析那种使用多行代表一个记录的数据,仅仅设置FS变量是不够的。在这种情况,我们也需要修改记录分隔变量(RS)。RS变量告诉awk何时结束当前的记录并开始一个新的记录。
作为一个例子,让我们来看看我们要怎么完成“联邦证人保护计划”参与者地址列表数据的处理:
Jimmy the Weasel 100 Pleasant Drive San Francisco, CA 12345
Big Tony 200 Incognito Ave. Suburbia, WA 67890 |
理想情况下,我们希望awk把每三行地址当着一个单独的记录,而不是三个不同的记录。如果awk能把地址中的第一行当做第一个字段($1),把街道地址当做第二个字段($2),把城市、州、邮编当做$3,那么我们的代码将非常简单。下面的代码就是按我们的想法来做的:
BEGIN { FS="\n" RS="" } |
上面的代码,设置FS为"\n"就是告诉awk每行为一个字段。通过设置RS为"", 我们也告诉awk,不同的记录是用一个空白行分隔的。只要awk知道了输入数据的格式,他就能够帮我们做解析工作,后面的代码那就简单了。让我们来看一下这个完整的脚本,他将解析地址表并在每行输出一个地址记录,每个字段使用逗号分隔:
BEGIN { FS="\n" RS="" } { print $1 ", " $2 ", " $3 } |
如果把上面的脚本保存到address.awk文件中,地址数据存储到address.txt文件中,你就能通过输入下面的命令"awk -f address.awk address.txt"来执行脚本。代码的输出如下:
Jimmy the Weasel, 100 Pleasant Drive, San Francisco, CA 12345 Big Tony, 200 Incognito Ave., Suburbia, WA 67890 |
OFS和ORS
你能看到在处理address.awk脚本文件中的print语句时,awk在一行上一个接一个的连接字符串。我们使用这个特性在一行中的三个地址字段间插入一个逗号和一个空格”, ”。尽管这个方法是有用的,但他看起来确实有点别扭。我们可以通过设置OFS变量,让awk来帮我们插入分隔符,而不是逐字的插入”, ”。 看看这个代码片段:
print "Hello", "there", "Jim!" |
这行中的逗号并不是实际的字符串中的一部分。他们是告诉awk后面的"Hello", "there", 和"Jim!" 是三个单独的字段,应在这些字符串之间插入OFS变量值。默认情况下awk生成下面的输出:
Hello there Jim! |
这就告诉我们OFS的值默认是” ”(单个空格字符)。然而,我们可以很容易的重新定义OFS,这样awk就能插入我们想要的字段分隔符。这儿是个修订版的address.awk程序,使用OFS输出中间的”, ”字符串:
BEGIN { FS="\n" RS="" OFS=", " } { print $1, $2, $3 } |
Awk也有一个特殊的变量ORS(输出记录分隔符)。ORS的默认值是”\n”,通过设置这个变量我们能控制在print语句后面自动打印的字符。默认的ORS值引起每个print语句输出一个换行符。如果我们想要输出两倍行距,我们可以设置ORS为”\n\n”。或者,如果我们想要使用单个空格分隔记录(不是使用换行),可以设置ORS为” ”。
多行到表格
假设我们写了一个脚本转换我们的地址本到一个tab分隔的,每个记录为一个单行的格式,以便于导入到电子表格中。使用我们刚小修改过的adress.awk版本,很明显它只能处理三行的地址模式。如果awk遇到下面的四行地址格式,第四行将被扔掉,不会被打印出来:
Cousin Vinnie Vinnie's Auto Shop 300 City Alley Sosueme, OR 76543 |
如果我们的代码考虑到每个记录的字段数,按顺序把每个字段打印出来,将会很好的处理这种情况。现在的代码只打印地址的头三个字段。而下面的代码可以完成我们想要的:
BEGIN { FS="\n" RS="" ORS="" }
{ x=1 while ( x<NF ) { print $x "\t" x++ } print $NF "\n" } |
首先,我们设置FS为"\n",RS为"",因此awk能像前面一样正确处理多行地址。 然后,我们设置ORS为"",这样每次调用print语句的末尾就不会输出一个换行。这意味着如果我们想要后面的任何文字从一个新行开始,我们需要显示的给出print "\n"。
在主代码块中,我们创建了一个叫x的变量,它记录我们当前处理的字段号。开始设置为1,然后我们使用一个while循环去迭代除最后一个字段外的其他所有字段,打印当前字段并加一个tab符号。最后,我们打印最后一个字段外加一个换行符。因为ORS被设置为"",print命令不会为我们输出换行符。程序的输出正是我们想要的这样:
Jimmy the Weasel 100 Pleasant Drive San Francisco, CA 12345 Big Tony 200 Incognito Ave. Suburbia, WA 67890 Cousin Vinnie Vinnie's Auto Shop 300 City Alley Sosueme, OR 76543 |
我们这样的输出不是很漂亮,不过tab分隔的格式很容易导入到电子表格中。
循环结构
我们已经见过了awk的while循环结构,它跟C语言的while循环是一样的。Awk也有一个"do...while"循环,它不像标准的while循环是在循环体开始前计算循环条件,do…while在循环体后面计算循环条件。它有点像其他语言中的"repeat...until"循环结构。这儿是一个例子:
do...while example
{ count=1 do { print "I get printed at least once no matter what" } while ( count != 1 ) } |
因为循环条件是在循环体后面计算,"do...while"循环的代码至少会执行一次。换句话说,对于一个正常while循环,如果一开始循环条件为false,它就一次都不会执行。
For循环
Awk允许你创建for循环,跟while一样,它也是跟C语言中的for循环一样的:
for ( initial assignment; comparison; increment ) { code block } |
这儿是一个例子:
for ( x = 1; x <= 4; x++ ) { print "iteration",x } |
这个代码片段,将打印下面的内容:
iteration 1 iteration 2 iteration 3 iteration 4 |
Break和continue
像C语言一样,awk也提供了break和continue语句。这些语句为awk的多种循环结构提供了良好的控制方法。这儿的代码片段是绝对需要break语句的:
while (1) { print "forever and ever..." } |
因为1总是true,这个while循环将一直运行下去。下面这个循环只执行10次:
x=1 while(1) { print "iteration",x if ( x == 10 ) { break } x++ } |
这儿使用break语句来跳出最里面的循环。"break"会导致循环立即终止,并开始执行循环代码后面的语句。
Continue语句也实现了终止功能,像下面这样工作:
x=1 while (1) { if ( x == 4 ) { x++ continue } print "iteration",x if ( x > 20 ) { break } x++ } |
这个代码将打印"iteration 1" 到 "iteration 21"的序列,但不会打印出 "iteration 4". 如果迭代到第4次,x自增1并执行调用continue语句,continue语句导致awk立即开始下一次循环而不再执行循环体内后面的语句。跟break语句一样,continue语句可以在任何种类的循环中使用。在for循环中使用continue时,循环控制变量会自动增加。这儿是一个功能相同的for循环:
for ( x=1; x<=21; x++ ) { if ( x == 4 ) { continue } print "iteration",x } |
这里并不需要像在while循环中一样,在continue之前调用x++,因为for循环的自增x是自动调用的。
数组
听说awk有数组,你可能会很高兴。然而,在awk中数组索引习惯从1开始,而不是从0开始:
myarray[1]="jim" myarray[2]=456 |
Awk遇到第一个赋值语句时,会自动创建myarray数组,myarray[1]的值被设置为"jim"。第二个赋值语句执行后myarray数组有了两个元素:
迭代数组
一旦定义了数组,awk有很方便的机制去迭代数组的元素,如下所示:
for ( x in myarray ) { print myarray[x] } |
这段代码将打印出myarray数组中的每个元素。当在for循环中使用这个特殊的in形式时,awk将依次把myarray中存在的索引分配给x(循环控制变量),每次分配后都执行循环体代码。尽管这是非常方便的awk特性,但它有个缺点——当awk循环获取数组索引时,并不遵循特定的顺序。这意味着没有方法知道上面的代码输出是否会像下面一样:
Jim 456 |
或者
456 Jim |
宽泛的套用一下《阿甘正传》中的话,迭代数组内容就像一个巧克力盒子——你绝不会准确的知道你将得到什么。这是awk数组索引的字符串特性在捣鬼,我们现在来看一下这个特性。
数组的字符串索引
在前面的文章中,我们知道awk实际使用字符串格式存储数字值。尽管awk执行了必要的转换,他也为看起来奇怪的代码打开了一扇门:
a="1" b="2" c=a+b+3 |
这个代码执行后,c的值等于6。因为awk是字符串型的,字符串"1"加上字符串"2"的功能跟数值1加数值2的功能没有不同。两种情况下awk都成功执行数学运算。Awk的字符串型特征非常有趣——你可能想知道如果我们使用字符串作为数组索引到底会发生什么。作为示例,我们看看下面的代码:
myarr["1"]="Mr. Whipple" print myarr["1"] |
正如你所期待的,这个代码将打印 "Mr. Whipple"。但是如果我们去掉第二个索引"1" 的引号会怎样呢?
myarr["1"]="Mr. Whipple" print myarr[1] |
猜这个代码片段的结果有些困难。Awk会把myarr["1"]和myarr[1]当做两个单独的元素呢,还是指的相同的元素?这个问题的答案是他们指的相同的元素,跟第一个代码块一样,awk将打印"Mr. Whipple"。尽快看起来奇怪,awk在幕后一直以来都为数组使用字符串索引!
知道了这个奇怪的事实后,我们中的有些人可能会尝试执行一些像下面一样的怪异代码:
myarr["name"]="Mr. Whipple" print myarr["name"] |
这个代码不仅不会报错,它的功能也跟前面的例子一样,将打印出"Mr. Whipple" ! 正如你所见,awk不会限制我们使用纯整数索引;如果我们想的话,我们能使用字符串索引,这不会出现问题。我们使用非整型数组索引时,比如myarray["name"],数组就叫联合数组(associative arrays)。技术上,当我们使用一个字符串索引时,awk在幕后并不会有不同的动作(因为即使你使用“整数”索引,awk也把它当做字符串)。然而,你应该仍然叫他们联合数组——它听起来很酷并且会让你的老板印象深刻。字符串型索引将是我们的小秘密。 ;)
数组工具
当提及数组时,awk给了我们很多灵活性。我们能使用字符串索引,他也不会要求我们的索引使用连续的数值序列(例如,我们可以定义myarr[1]和myarr[1000],其他的元素都不定义)。尽管所有这些都很有帮助,在某种情况下它也可能引起混淆。幸运地是,awk提供了几个方便的特性帮我们让数组更好管理。
首先,我们能删除数组元素。如果你想删除你的数组fooarray的元素1,执行下面的语句:
delete fooarray[1] |
并且,如果你想看一个特定的元素是否存在,你可以像下面一样使用特殊的"in" 布尔表达式:
if ( 1 in fooarray ) { print "Ayep! It's there." } else { print "Nope! Can't find it." } |
下次…
本篇中我们已经讨论了大量的基础内容。下次,我将向你展示awk的数学和字符串函数并教你如何创建自己的函数,以此来补足你的awk知识。我也将带领你创建一个支票结算程序。到那时我会鼓励你写一些自己的awk程序,还是去看看下面的资源吧。
Resources
- Read Awk by example, Part 1 on developerWorks.
- If you'd like a good old-fashioned book, O'Reilly's sed & awk, 2nd Edition is a wonderful choice.
- Be sure to check out the comp.lang.awk FAQ. It also contains lots of additional awk links.
- Patrick Hartigan's awk tutorial is packed with handy awk scripts.
第二部分结束