Awk实例 第三部分(系列结束)


Common threads: Awk by example, Part 3

字符串函数和支票?

概要:在awk系列的最后一篇,Daniel向你介绍awk重要的字符串函数并展示如何从开始写一个完整的支票结算程序。 一路走下来,你将学会如何写自己的函数以及如何使用awk的多维数组。本篇结束时,你将有更多的awk经验,可以编写更加强大的脚本。

格式化输出

尽管大多数时候awk的print语句都能满足要求,但有时还是不够。Awk还提供了两个很好用的老朋友printf()和sprintf()。是的,这些函数跟他们在C语言中的同名函数是一样的。printf()将在标准输出设备上打印一个格式化字符串,而sprintf()将返回一个格式化的字符串,这个返回值还可以赋给某个变量。 如果你还不熟悉printf()和sprintf(),找一本C语言的介绍性书来读一读,你将很快能了解这两个基本的函数。你也能在Linux系统上输入"man 3 printf"来查看printf()的man说明。

这儿是一些awk的sprintf()和printf()的代码例子。如你所见,所有用法看起来都跟C语言中一样:

x=1

b="foo"

printf("%s got a %d on the last test\n","Jim",83)

myout=("%s-%d",b,x)

print myout

这个代码将打印:

Jim got a 83 on the last test

foo-1

字符串函数

Awk有很多的字符串函数,这是个好事。因为在awk中你不能像C、C++和python等其他语言一样,把字符串当做一个字符数组来处理,所以你真的很需要字符串函数。例如你执行下面的代码:

mystring="How are you doing today?"

print mystring[3]

你将得到一个像下面一样的错误:

awk: string.gawk:59: fatal: attempt to use scalar as array

哦,好吧!尽管不如Python的序列类型方便,awk的字符串函数还是可以做这些工作的。让我们来看看这些函数。

首先,我们有基本的length()函数,它会返回字符串的长度。这儿是如何使用它的例子:

print length(mystring)

这段代码将打出下面的值:

24

OK,让我们继。下一个字符串函数叫index,他将返回一个子串在另一个字符串中出现的位置,如果找不到这个子串,就返回0。使用mystring作为例子,我们可以这样调用:

print index(mystring,"you")

Awk打印:

9

我们继续来看两个更容易的函数, tolower() 和toupper()。你可能猜到了,这些函数将分别返回全部是大写或全部是小写的字符串。要注意的是tolower() 和toupper()返回新的字符串,而不是修改原来的字符串。这段代码:

print tolower(mystring)

print toupper(mystring)

print mystring

…将产生下面的输出:

how are you doing today?

HOW ARE YOU DOING TODAY?

How are you doing today?

到目前为止,一切顺利,但如果我想从字符串中提取一个子串或者甚至是单个字符,那该咋办呢?substr()函数就是干这个的。这儿显示如何调用substr():

     

mysub=substr(mystring,startpos,maxlen)

Mystring应该是你想从中提取子串的一个字符串变量或者是一个原始的字符串。 startpos应该设置为开始字符的位置。 maxlen 应该包含你想要提取的子串的最大长度。注意我说的最大长度;如果length(mystring)的值小于startpos+maxlen,你的结果将被截断。substr()不会修改原始字符串,而是返回一个新字符串。这儿是一个例子:

print substr(mystring,9,3)

Awk将打出:

You

如果你经常使用那些用数组索引来访问字符串的语言编程,那么要牢记awk中是用substr()来代替这种方法的。你将用它来提取单个字符和子串,因为awk是一个基于字符串的语言,你会经常用到它的。

现在,我们来继续看一些更棒的函数,第一个叫做match()。Match() 有点像index(),除了像index()一样搜索子串,他还可以搜索正则表达式。Match()将返回匹配项的开始点,或者返回0(如果找不到匹配项)。另外,math()也会设置两个变量,分别叫RSTART和RLENGTH。RSTART包含返回值(第一个匹配项的位置),RLENGTH指出匹配的字符长度(如果找不到匹配项,则为-1)。使用RSTART、RLENGTH、substr()和一个小的循环,你能很容易的在你的字符串中迭代每个匹配项。这儿是一个match()调用的例子:

 

print match(mystring,/you/), RSTART, RLENGTH

Awk将打印:

9 9 3

字符串替换

现在,我们来看两个字符串替换函数sub()和gsub()。这两个家伙跟我们已经见过的函数有些轻微不同的地方在于:他们实际修改原始字符串。这儿是个模版,显示如何调用sub():

sub(regexp,replstring,mystring)

当你调用sub()时,它将在mystring中找到第一个匹配regexp的字符序列,并使用replstring替换这个字符序列。sub()和gsub()有相同的参数,他们唯一的不同点在于,sub()只替换第一个匹配项(如果有匹配项的话),而gsub()将执行一个全局替换,替换字符串中的所有匹配项。这儿是一个调用sub()和bsub()的例子:

sub(/o/,"O",mystring)

print mystring

mystring="How are you doing today?"

gsub(/o/,"O",mystring)

print mystring

因为第一个sub()直接修改了mystring,我们必须重置mystring到他的原始值。当执行上述的代码时,awk将输出下面的内容:

HOw are you doing today?

HOw are yOu dOing tOday?

当然,更加复杂的正则表达式也是可以的。我将留下来让你自己去测试一些复杂的正则表达式。

我们最后给你介绍一个叫做split()的函数,以此来结束字符串函数的内容。Split()的工作是用来分割一个字符串,并将分割后的多个部分放入一个使用整数索引的数组中。这儿是一个split()调用的例子:

numelements=split("Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",mymonths,",")

当调用split()时,第一个参数是待分割的字符串或者字符串变量。第二个参数是split()用来填充已分割部分到其中的数组名字。第三个参数指定分割字符串的分隔符。Split()返回分割后的字符串数量。Split()将每个分割后的字符串放到一个索引从1开始的数组中,因此下面的代码:

print mymonths[1],mymonths[numelements]

....将打印出:

Jan Dec

特殊字符串形式

一个快捷方法 –当调用length()、sub()、或 gsub()时, 你可以不写最后一个参数,awk将使用$0(当前的整行)代替。打印一个文件中每行的长度可使用下面的awk脚本:

          

{

      print length()

}

财务的乐趣

几周前,我决定使用awk写一个我自己的支票结算程序。我使用一个tab分隔的文本文件来记录最近的存款和提款记录。当时的想法是使用一个awk脚本来处理这个数据,脚本会自动合计总额并告诉我余额。这儿是我如何记录我所有的存取款事务到我的“ASCII支票”例子:

23 Aug 2000    food     -        -        Y        Jimmy's Buffet            30.25

这个文件中使用一个或多个tab符分隔每个字段。日期字段($1)后,有两个字段分别叫“消费类别”和“收入类别”。当我像上面这行一样输入一个消费类记录时,我在消费类别字段使用一个4个字符描述的简称,在收入类别字段使用一个“-”。上面的记录表示这个特殊的记录是一个“食品消费”。下面这儿是一个存款记录:

23 Aug 2000    -        inco     -        Y        Boss Man          2001.00

在这种情况下,我在消费类别字段使用一个“-”,在收入类别字段填了“inco”。“inco”是我的常规(薪水类别)收入的简称。使用类别简称允许我生成收入和支出分类。至于记录的其余部分,所有其他的字段都很容易自解释。Cleared字段("Y" or "N")记录该事务是否已经提交到我的银行账户。除此之外,还有个事务描述和一个正数表示的该事务发生的美元总额。

用来计算当前余额的算法不是很难。Awk仅需要一行接一行的读入每行。如果一行有消费类别但无收入类别(标记为“-”),这行就是借项。反之,如果一行有收入类别但无消费类别,这行就是贷项。如果一行既有消费类别又有收入类别,那么此行记录的是一个“类别转移”,也就是说该记录的美元金额值要从消费类别中减去,并增加到收入类别。此外,所有这些类别都是虚拟的,但对于跟踪收入和支出以及预算编制很有用。

代码

是时候来看看代码了。我们将从第一行开始,然后跟一个BEGIN块和一个函数定义:


balance, part 1

     

#!/usr/bin/env awk -f

BEGIN {

      FS="\t+"

      months="Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"

}

 

function monthdigit(mymonth) {

      return (index(months,mymonth)+3)/4

}

在任何awk脚本中增加第一行“#!...”,将允许直接从shell中执行该脚本,当然需要先用"chmod +x myscript"给脚本添加执行权限。剩下的行定义了我们的BEGIN块,在awk开始处理我们的支票文件前会执行BEGIN块的内容。我们设置了FS变量为“\t+”,这告诉awk我们使用一个或多个tab分隔字段。另外,我们定义了一个叫months的字符串,monthdigit()函数会用到它,我们马上来介绍这个函数。

最后三行展示了如何定义你自己的awk函数。定义格式很简单——输入“function”,然后跟着函数名以及放在圆括号中的由逗号分隔的参数列表。之后,将函数需要执行的代码块放在一个大括号“{}”中。所有函数都能访问全局变量(比如我们的months变量)。此外,awk提供了一个返回语句,允许函数返回一个值,这个返回操作跟C、python或其他语言中的返回语句操作是类似的。这个特殊的函数将一个3个字符表示的月度名字转换为等价的数字值。例如,下面这个代码:

print monthdigit("Mar")

....将打印:

3

现在,让我们继续来写更多的函数。

财务函数

这儿是三个为我们执行记账功能的函数。我们很快会看到的主函数将按顺序处理支票文件的每一行记录,处理一行记录时会调用这三个函数中的一个来将适当的事务记录到一个awk数组中。有三个基本类型的事务,他们是贷(doincome)、借(doexpense)和转移(dotransfer)。你可能注意到了这三个函数都接受一个叫做mybalance的参数。mybalance是一个二维数组的占位符,我们将传递一个二维数组作为入参。到目前为止我们还没有处理过二维数组,然而正如下面你将看到的,处理二维数组的语法相当简单。仅仅使用一个逗号来分隔维度,就可以处理了。

我们将按如下所述的方式把信息记录到mybalance中。数组第一个维度的范围是0到12,用来代表月份,0表示是整年。第二个维度是四字符表示的类别,比如“food”或“inco”,这是我们实际在处理的类别。所以,要找到整年的食物类别结余,你可以查看mybalance[0,"food"]。要找到6月份的收入,你可以查看mybalance[6,"inco"]。


balance, part 2

                     

function doincome(mybalance) {

      mybalance[curmonth,$3] += amount

      mybalance[0,$3] += amount

}

 

function doexpense(mybalance) {

      mybalance[curmonth,$2] -= amount

      mybalance[0,$2] -= amount

}

 

function dotransfer(mybalance) {

      mybalance[0,$2] -= amount

      mybalance[curmonth,$2] -= amount

      mybalance[0,$3] += amount

      mybalance[curmonth,$3] += amount

}

当调用doincome()或其他任何函数时,我们在两个地方记录当前事务——mybalance[0,category] 和 mybalance[curmonth, category],分别是全年的类别结余和当前月的类别结余。这允许我们在后面产生年度或月度收支分类报表。

如果你仔细看看这些函数,你将发现mybalance引用的数组是传入的参数。此外,我们也用到了几个全局变量:curmonth,保存的是当前记录的月份数字值,$2(消费类别),$3(收入类别),以及金额($7,美元金额)。当doincome()和他的朋友们被调用时,所有这些变量都已经正确的设置为为当前正在处理的记录的对应值。

主代码块

这儿是主代码块,它包含解析输入数据的每一行的代码。记住,因为我们已经正确设置了FS变量,我们能使用$1引用第一个字段,使用$2引用第二个字段,以此类推。当doincome()和它的朋友们被调用时,函数能访问curmonth、$2、$3和金额的当前值。看一下这个代码,我们后面来解释。


balance, part 3

 

{

      curmonth=monthdigit(substr($1,4,3))

      amount=$7

     

      #record all the categories encountered

      if ( $2 != "-" )

               globcat[$2]="yes"

      if ( $3 != "-" )

               globcat[$3]="yes"

 

      #tally up the transaction properly

      if ( $2 == "-" ) {

               if ( $3 == "-" ) {

                        print "Error: inc and exp fields are both blank!"

                        exit 1

               } else {

                        #this is income

                        doincome(balance)

                        if ( $5 == "Y" )

                                 doincome(balance2)

               }

      } else if ( $3 == "-" ) {

               #this is an expense

               doexpense(balance)

               if ( $5 == "Y" )

                        doexpense(balance2)

      } else {

               #this is a transfer

               dotransfer(balance)

               if ( $5 == "Y" )

                        dotransfer(balance2)

      }                        

}

在主代码块中,头两行设置curmonth为一个1到12之间的整数值,并设置amount为字段7的值(为了代码更容易理解)。然后我们有四行比较有趣的代码,我们在一个叫globcat的数组中写入数据。Globcat,或者叫全局分类数组,是用来记录在支票文件中遇到的所有分类——"inco", "misc", "food", "util"等等。例如,如果$2== "inco",我们设置globcat["inco"]为"yes"。以后我们能使用简单的"for (x in globcat)"循环来迭代访问我们的类别列表。

后面的二十多行,我们分析$2和$3,并适当的记录事务。如果 $2=="-" 并且$3!="-", 表明这是一个收入记录,所以我们调用doincome()。 反之,我们调用doexpense();如果$2和$3都包含分类,我们调用dotransfer()。每次我们都传递"balance"数组到这些函数,以便于适当的数据都记录在这个数组中。

你也注意到了有几行说"if ( $5 == "Y" ), 在balance2中记录相同的事务"。 我们这儿到底是干什么呢?你应该还记得$5的值要么是 "Y"要么是"N",表示是否当前事务已经提交到银行账户。 因为仅提交到银行账户的事务记录在balance2中,balbnce2将包含实际账户的结余,而"balance"将包含所有的事务,无论事务是否已经提交。你能使用balance2来核对你的数据条目(因为它应该与你银行账号余额保持一致),使用"balance"来确保你不会透支你的账户(因为它会考虑到任何你已经写了但还没兑现的支票)。

生成报告

在主代码块重复的处理完每个输入记录后,我们现在有了一个相对完整的按月按类别的借贷记录。现在,我们需要做的事情是写一个END块来生成报告,此种情况下一个最适和的代码如下:

END {

      bal=0

      bal2=0  

      for (x in globcat) {

               bal=bal+balance[0,x]

               bal2=bal2+balance2[0,x]   

       }

       printf("Your available funds: %10.2f\n", bal)

       printf("Your account balance: %10.2f\n", bal2)     

}

这段代码打印出一个看起来如下的概要报告:

Your available funds:    1174.22

Your account balance:    2399.33

在我们的END块中,我们使用"for (x in globcat)"结构迭代每一个类别,基于所有的事务记录,计算出结余。我们实际上计算出两个余额,一个是可用的资金,另一个是账户余额。要执行这个程序来处理你自己记录在一个叫"mycheckbook.txt"文件中的财务事项,只要将上面的所有代码放到一个命名为"balance"的文本文件中,使用"chmod +x balance"修改文件的权限,然后输入"./balance mycheckbook.txt"开始执行。这个结算脚本将计算你所有的事务并打印出两行余额摘要。

增强

我使用了一个这个程序的更加高级的版本来管理我个人和生意上的财务事项。我的版本(因为空间所限,我无法放到这里)打印出按月度的收入和支出,包括年度总和,净收入和一推其他的东西。更胜一筹的是,我使用HTML格式输出数据,以便我能在一个Web浏览器中查看J如果你觉得这个程序有用,我鼓励你增加这些特性到这个脚本中。你并不需要配置他来记录任何附加的信息,所有你需要的信息都在balance和balance2中。仅更新END块就可以达到要求!

我希望你喜欢这个系列。关于awk更多的信息请参考下面列出的资源。

 

Resources

  • Read Daniel's earlier installments in the awk series: Awk by example, Part 1 and Part 2 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.

系列结束

相关内容