声明: 本文的原始版本最初发表于IBM developerWorks,现在所有权归属Westtech Information Services。本文档是原始文档的更新版本,包含了Gentoo Linux文档团队所做的很多改进。 现在无人积极维护本文档。 |
Awk实例,第2部分
1.
记录、循环和数组
多行记录
awk是一种用于读取和处理结构化数据(如系统的/etc/passwd文件)的极佳工具。/etc/passwd是UNIX用户数据库,并且是用冒号定界的文本文件,它包含许多重要信息,包括所有现有用户帐户和用户标识,以及其它信息。在我的前一篇文章中,我演示了awk如何轻松地分析这个文件。我们只须将FS(字段分隔符)变量设置成":"。
正确设置了FS变量之后,就可以将awk配置成分析几乎任何类型的结构化数据,只要这些数据是每行一个记录。然而,如果要分析占据多行的记录,仅仅依靠设置FS是不够的。在这些情况下,我们还需要修改RS记录分隔符变量。RS变量告诉awk当前记录什么时候结束,新记录什么时候开始。
譬如,让我们讨论一下如何完成处理“联邦证人保护计划”所涉及人员的地址列表的任务:
代码 1.1: “联邦证人保护计划”所涉及人员的地址列表 |
Jimmy the Weasel
100 Pleasant Drive
San Francisco, CA 12345
Big Tony
200 Incognito Ave.
Suburbia, WA 67890
|
理论上,我们希望awk将每3行看作是一个独立的记录,而不是三个独立的记录。如果awk将地址的第一行看作是第一个字段($1),街道地址看作是第二个字段($2),城市、州和邮政编码看作是第三个字段$3,那么这个代码就会变得很简单。以下就是我们想要得到的代码:
代码 1.2: 从地址新建一个字段 |
BEGIN {
FS="\n"
RS=""
}
|
在上面这段代码中,将FS设置成"\n"告诉awk每个字段都占据一行。通过将RS设置成"",还会告诉awk每个地址记录都由空白行分隔。一旦awk知道是如何格式化输入的,它就可以为我们执行所有分析工作,脚本的其余部分很简单。让我们研究一个完整的脚本,它将分析这个地址列表,并将每个记录打印在一行上,用逗号分隔每个字段。
代码 1.3: 完整的脚本 |
BEGIN {
FS="\n"
RS=""
}
{ print $1 ", " $2 ", " $3 }
|
如果这个脚本保存为address.awk,地址数据存储在文件address.txt 中,可以通过输入"awk -f address.awk address.txt"来执行这个脚本。此代码将产生以下输出:
代码 1.4: 脚本输出 |
Jimmy the Weasel, 100 Pleasant Drive, San Francisco, CA 12345
Big Tony, 200 Incognito Ave., Suburbia, WA 67890
|
OFS和ORS
在address.awk的print语句中,可以看到awk会连接(合并)一行中彼此相邻的字符串。我们使用此功能在同一行上的三个字段之间插入一个逗号和空格(", ")。这个方法虽然有用,但比较难看。与其在字段间插入","字符串,倒不如让通过设置一个特殊awk变量OFS,让awk完成这件事。请参考下面这个代码片断。
代码 1.5: 示例代码片断 |
print "Hello", "there", "Jim!"
|
这行代码中的逗号并不是实际文字字符串的一部分。事实上,它们告诉awk,"Hello"、"there"和"Jim!"是单独的字段,并且应该在每个字符串之间打印OFS变量。缺省情况下,awk产生以下输出:
代码 1.6: awk产生的输出 |
Hello there Jim!
|
这是缺省情况下的输出结果,OFS被设置成" ",单个空格。不过,我们可以方便地重新定义OFS,这样awk将插入我们中意的字段分隔符。以下是原始address.awk程序的修订版,它使用OFS来输出那些中间的",
"字符串:
代码 1.7: 重新定义OFS |
BEGIN {
FS="\n"
RS=""
OFS=", "
}
{ print $1, $2, $3 }
|
awk还有一个特殊变量ORS,全称是“输出记录分隔符”。通过设置缺省为换行 ("\n") 的OFS,我们可以控制在print语句结尾自动打印的字符。缺省ORS值会使awk在新行中输出每个新的print语句。如果想使输出的间隔翻倍,可以将ORS设置成"\n\n"。或者,如果想要用单个空格分隔记录(而不换行),将ORS设置成""。
将多行转换成用tab分隔的格式
假设我们编写了一个脚本,它将地址列表转换成每个记录一行,且用tab定界的格式,以便导入电子表格。使用稍加修改的address.awk之后,就可以清楚地看到这个程序只适合于三行的地址。如果awk遇到以下地址,将丢掉第四行,并且不打印该行:
代码 1.8: Sample entry |
Cousin Vinnie
Vinnie's Auto Shop
300 City Alley
Sosueme, OR 76543
|
要处理这种情况,代码最好考虑每个字段的记录数量,并依次打印每个记录。现在,代码只打印地址的前三个字段。以下就是我们想要的一些代码:
代码 1.9: 改进后的代码 |
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循环(一种awk循环结构,等同于C语言中的while循环),对于所有记录(最后一个记录除外)重复打印记录和tab字符。最后,打印最后一个记录和换行;此外,由于将ORS设置成"",print将不输出换行。程序输出如下,这正是我们所期望的:
代码 1.10: 我们想要的输出。不算漂亮,但用tab定界,以便于导入电子表格 |
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
|
循环结构
我们已经看到了awk的while循环结构,它等同于相应的C语言while循环。awk还有"do...while"循环,它在代码块结尾处对条件求值,而不象标准while循环那样在开始处求值。它类似于其它语言中的"repeat...until"循环。以下是一个示例:
代码 1.11: do...while示例 |
{
count=1
do {
print "I get printed at least once no matter what"
} while ( count != 1 )
}
|
与一般的while循环不同,由于在代码块之后对条件求值,"do...while"循环永远都至少执行一次。换句话说,当第一次遇到普通while循环时,如果条件为假,将永远不执行该循环。
for循环
awk允许创建for循环,它就象while循环,也等同于C语言的for循环:
代码 1.12: 循环示例 |
for ( initial assignment; comparison; increment ) {
code block
}
|
以下是一个简短示例:
代码 1.13: 简短示例: |
for ( x = 1; x <= 4; x++ ) {
print "iteration",x
}
|
此段代码将打印:
代码 1.14: 上面代码的输出信息 |
iteration 1
iteration 2
iteration 3
iteration 4
|
break和continue
此外,如同C语言一样,awk提供了break和continue语句。使用这些语句可以更好地控制awk的循环结构。以下是迫切需要break语句的代码片断:
代码 1.15: break语句所需的代码片断 |
while (1) {
print "forever and ever..."
}
|
while死循环1永远代表是真,这个while循环将永远运行下去。以下是一个只执行十次的循环:
代码 1.16: 只执行10次的循环 |
x=1
while(1) {
print "iteration",x
if ( x == 10 ) {
break
}
x++
}
|
这里,break语句用于“逃出”最深层的循环。"break"使循环立即终止,并继续执行循环代码块后面的语句。
continue语句补充了break,其作用如下:
代码 1.17: 补充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并调用continue语句,该语句立即使awk开始执行下一个循环迭代,而不执行代码块的其余部分。如同break一样,continue语句适合各种awk迭代循环。在for循环主体中使用时,continue将使循环控制变量自动增加。以下是一个等价循环:
代码 1.18: 等价循环 |
for ( x=1; x<=21; x++ ) {
if ( x == 4 ) {
continue
}
print "iteration",x
}
|
在while循环中时,在调用continue之前没有必要增加x,因为for循环会自动增加x。
数组
如果您知道awk可以使用数组,您一定会感到高兴。然而,在awk中,数组下标通常从1开始,而不是0:
代码 1.19: awk数组示例 |
myarray[1]="jim"
myarray[2]=456
|
当awk遇到第一个赋值语句时,它将创建myarray,并将元素myarray[1]设置成"jim"。在执行了第二个赋值语句后,数组就有两个元素了。
定义之后,awk有一个便利的机制来迭代数组元素,如下所示:
代码 1.20: 数组迭代 |
for ( x in myarray ) {
print myarray[x]
}
|
这段代码将打印数组myarray中的每一个元素。当对于for使用这种特殊的"in"形式时,awk将myarray的每个现有下标依次赋值给x(循环控制变量),每次赋值以后都循环一次循环代码。虽然这是一个非常方便的awk功能,但它有一个缺点──当awk在数组下标之间轮转时,它不会依照任何特定的顺序。那就意味着我们不能知道以上代码的输出是:
还是
代码 1.22: 上面代码的另一种输出 |
456
jim
|
套用阿甘(Forrest Gump)的话来说,迭代数组内容就像一盒巧克力──您永远不知道将会得到什么。因此有必要使awk数组“字符串化”,我们现在就来研究这个问题。
数组下标字符串化
在我的前一篇文章中,我演示了awk实际上以字符串格式来存储数字值。虽然awk要执行必要的转换来完成这项工作,但它却可以使用某些看起来很奇怪的代码:
代码 1.23: 奇怪的代码 |
a="1"
b="2"
c=a+b+3
|
执行了这段代码后,c等于6。由于awk是“字符串化”的,添加字符串"1"和"2"在功能上并不比添加数字1和2难。这两种情况下,awk都可以成功执行运算。awk的“字符串化”性质非常可爱──您可能想要知道如果使用数组的字符串下标会发生什么情况。例如,使用以下代码:
代码 1.24: 示例代码 |
myarr["1"]="Mr. Whipple"
print myarr["1"]
|
可以预料,这段代码将打印"Mr. Whipple"。但如果去掉第二个"1"下标中的引号,情况又会怎样呢?
代码 1.25: 去掉引号的代码 |
myarr["1"]="Mr. Whipple"
print myarr[1]
|
猜想这个代码片断的结果比较难。awk将myarr["1"]和myarr[1]看作数组的两个独立元素,还是它们是指同一个元素?答案是它们指的是同一个元素,awk将打印"Mr. Whipple",如同第一个代码片断一样。虽然看上去可能有点怪,但awk在幕后却一直使用数组的字符串下标!
了解了这个奇怪的真相之后,我们中的一些人可能想要执行类似于以下的古怪代码:
代码 1.26: 古怪的代码 |
myarr["name"]="Mr. Whipple"
print myarr["name"]
|
这段代码不仅不会产生错误,而且它的功能与前面的示例完全相同,也将打印"Mr. Whipple"!可以看到,awk并没有限制我们使用纯整数下标;如果我们愿意,可以使用字符串下标,而且不会产生任何问题。只要我们使用非整数数组下标,如myarr["name"],那么我们就在使用关联数组。从技术上讲,如果我们使用字符串下标,awk的后台操作并没有什么不同(因为即便使用“整数”下标,awk还是会将它看作是字符串)。但是,应该将它们称作关联数组──它听起来很酷,而且会给您的上司留下印象。字符串化下标是我们的小秘密。;)
数组工具
谈到数组时,awk给予我们许多灵活性。可以使用字符串下标,而且不需要连续的数字序列下标(例如,可以定义myarr[1]和myarr[1000],但不定义其它所有元素)。虽然这些都很有用,但在某些情况下,会产生混淆。幸好,awk提供了一些实用功能有助于使数组变得更易于管理。
首先,可以删除数组元素。如果想要删除数组fooarray的元素1 ,输入:
代码 1.27: 删除数组元素 |
delete fooarray[1]
|
而且,如果想要查看是否存在某个特定数组元素,可以使用特殊的"in"布尔运算符,如下所示:
代码 1.28: 查看是否存在某个特定数组元素 |
if ( 1 in fooarray ) {
print "Ayep! It's there."
} else {
print "Nope! Can't find it."
}
|
下一篇
本文中,我们已经讨论了许多基础知识。下一篇中,我将演示如何使用awk的数学运算和字符串函数,以及如何创建您自己的函数,使您完全掌握awk知识。我还将指导您创建支票簿结算程序。那时,我会鼓励您编写自己的awk程序。请查阅以下参考资料。
2.
参考资料
|