【译】 AWK教程指南 7AWK应用实例

时间:2022-02-26 16:40:42

  本节将示范一个统计上班到达时间及迟到次数的程序。

  这程序每日被执行时将读入两个数据文件:

    * 员工当日到班时间的数据文件 ( 如下列的 arr.dat )

    * 存放员工当月迟到累计次数的文件

  当程序执行执完毕后将更新第二个数据文件的数据(迟到次数),并打印当日的报表。这程序将分成下列数小节逐步完成,其大纲如下:

7.1 在到班资料文件 arr.dat 之前增加一行抬头"ID Number Arrvial Time",并产生报表输出到文件today_rpt1 中。

  <在awk中如何将数据输出到文件>

7.2 将 today_rpt1 上的数据按员工代号排序,并加注执行当日日期;产生文件 today_rpt2

  <awk中如何运用系统资源及awk中Pipe的特性>

7.3 将awk程序包含在一个shell script文件中

7.4 于 today_rpt2 每日报表上,迟到者之前加上"*",并加注当日平均到班时间;产生文件 today_rpt3

7.5 从文件中读取当月迟到次数,并根据当日出勤状况更新迟到累计数。

  <使用者在awk中如何读取文件数据>

  某公司其员工到勤时间文件内容如下,取名为 arr.dat。文件中第一栏为员工代号,第二栏为到达时间。本范例中,将使用该文件为数据文件。

                    【译】 AWK教程指南 7AWK应用实例

7.1 重定向输出到文件

  awk中并未提供如 C 语言中的fopen() 指令,也没有fprintf() 文件输出这样的指令。但awk中任何输出函数之后皆可借助使用与UNIX 中类似的 I/O 重定向符,将输出的数据重定向到指定的文件;其符号仍为 > (输出到一个新产生的文件) 或 >> ( 添加输出的数据到文件末尾 )。

例:在到班数据文件 arr.dat 之前增加一行抬头如下:"ID Number Arrival Time",并产生报表输出到文件 today_rpt1中。

  建立如下文件并取名为reformat1.awk

    BEGIN{ print " ID Number Arrival Time" > "today_rpt1"
        print "===========================" > "today_rpt1"
    }
    { printf(" %s %s\n", $,$ ) > "today_rpt1" }

  执行:    

    $ awk -f reformat1.awk arr.dat

  执行后将产生文件 today_rpt1,其内容如下:

              【译】 AWK教程指南 7AWK应用实例

说 明:

  1. awk程序中,文件名称 today_rpt1 的前后须以" (双引号)括住,表示 today_rpt1 为一字符串常量。若未以"括住,则 today_rpt1 将被awk解释为一个变量名称。

  在awk中任何变量使用之前,并不须事先声明。其初始值为空字符串(Null string) 或 0。因此程序中若未以 " 将 today_rpt1 括住,则 today_rpt1 将是一变量,其值将是空字符串,这会在执行时造成错误(Unix 无法帮您开启一个以空字符串为文件名的文件)。

  因此在编辑awk程序时,须格外留心。因为若敲错变量名称,awk在编译程序时会认为是一新的变量,并不会察觉。因此往往会造成运行时错误。

  2. BEGIN 为awk的保留字,是 Pattern 的一种。

  以 BEGIN 为 Pattern 的 Actions 于awk程序刚被执行尚未读取数据文件时被执行一次,此后便不再被执行。

  3. 读者或许觉得本程序中的I/O重定向符号应使用 " >>" (append)而非 " >"。

  本程序中若使用 ">" 将数据重定向到 today_rpt1,awk 第一次执行该指令时会产生一个新文件 today_rpt1,其后再执行该指令时则把数据追加到today_rpt1文件末,并非每执行一次就重开一个新文件。

  若采用">>"其差异仅在第一次执行该指令时,若已存在today_rpt1则 awk 将直接把数据append在原文件的末尾。

  这一点,与UNIX中的用法不同。

7.2 使用系统资源

  awk程序中很容易使用系统资源。这包括在程序中途调用 Shell 命令来处理程序中的部分数据;或在调用 Shell 命令后将其产生的结果交回 awk 程序(不需将结果暂存于某个文件)。这一过程是借助 awk 所提供的管道 (虽然有些类似 Unix 中的管道,但特性有些不同),及一个从 awk 中调用 Unix 的 Shell 命令的语法来达成的。

例: 承上题,将数据按员工ID排序后再输出到文件 today_rpt2,并于表头附加执行时的日期。

分 析:

  1. awk 提供与 UNIX 用法近似的 pipe,其记号亦为 "|"。其用法及含意如下:

    awk程序中可接受下列两种语法:

      a.语法

      awk output 指令 | "Shell 接受的命令"

      (如: print $1,$2 | "sort -k 1")

       b.语法 

      "Shell 接受的命令" | awk input 指令

      (如: "ls " | getline)

      注: awk input 指令只有 getline 一个。

         awk output 指令有 print, printf() 两个。

  2. 在a 语法中,awk所输出的数据将转送往 Shell,由 Shell 的命令进行处理。以上例而言,print 所输出的数据将经由 Shell 命令 "sort -k 1" 排序后再送往屏幕(stdout)。

  上列awk程序中,"print$1, $2" 可能反复执行很多次,其输出的结果将先暂存于 pipe 中,等到该程序结束时,才会一并进行 "sort -k 1"。

  须注意两点:不论 print $1, $2 被执行几次,

            "sort -k 1" 的执行时间是 "awk程序结束时",

            "sort -k 1" 的执行次数是 "一次"。

  3. 在 b 语法中,awk将先调用 Shell 命令。其执行结果将通过 pipe 送入awk程序,以上例而言,awk先让 Shell 执行 "ls",Shell 执行后将结果存于 pipe,awk指令 getline 再从 pipe 中读取数据。

  使用本语法时应留心:

    以上例而言,awk "立刻"调用 Shell 来执行 "ls",执行次数是一次。

    getline 则可能执行多次(若pipe中存在多行数据)。

  4. 除上列a、b二种语法外,awk程序中其它地方如出现像 "date", "cls", "ls"... 这样的字符串,awk只把它当成一般字符串处理。

  建立如下文件并取名为 reformat2.awk

    # 程序 reformat2.awk
    # 这程序用以练习awk中的pipe     BEGIN {
      "date" | getline   #Shell 执行 "date",getline 取得结果并以$0记录
      print " Today is " , $, $     > "today_rpt2"
      print "=========================" > "today_rpt2"
      print " ID Number Arrival Time" > "today_rpt2"
      close( "today_rpt2" )
    }
    { printf( "%s %s\n", $ ,$ ) | "sort -k 1 >> today_rpt2" }

  执行如下命令:    

    $ awk -f reformat2.awk arr.dat

  执行后,系统会自动将 sort 后的数据追加( Append; 因为使用 " >>") 到文件 today_rpt2末端。today_rpt2 内容如下:

              【译】 AWK教程指南 7AWK应用实例

说 明:

  1. awk程序由三个主要部分构成:

    i. Pattern { Action} 指令

     ii. 函数主体。 例如: function double( x ){ return 2*x } (参考第11节 Recursive Program )

    iii. Comment ( 以 # 开头识别之 )

  2. awk 的输入指令 getline,每次读取一行数据。若getline之后未接任何变量,则所读入的内容将以$0 记录;否则以所指定的变量储存之。

  以本例而言:

    执行 "date" | getline 后,

    $0 的值为 "Tue Nov 19 00:15:31 CST 2013" (笔者注:该时间为笔者本机上程序的执行时间)

  当 $0 的值被更新时,awk将自动更新相关的内置变量,如:$1,$2,..,NF。故 $2 的值将为"Nov",$3的值将为"19"。

  (有少数旧版的awk不允许即使用者自行更新(update)$0的值,或者更新$0时,它不会自动更新 $1,$02,..,NF。这情况下,可改用gawk或nawk。否则使用者也可自行以awk字符串函数split()来分隔$0上的数据)

  3. 本程序中 printf() 指令会被执行12次( 因为有arr.dat中有12行数据),但读者不用担心数据被重复sort了12次。当awk结束该程序时才会 close 这个 pipe,此时才将这12行数据一次送往系统,并调用 "sort -k 1 >> today_rpt2" 处理之。

  4. awk提供另一个调用Shell命令的方法,即使用awk函数         

    system("shell命令")

  例如:        

    awk '
    BEGIN{
      system("date > date.dat")
      getline < "date.dat"
      print "Today is ", $, $
    }
    '

  但使用 system( "shell 命令" ) 时,awk无法直接将执行中的部分数据输出给Shell 命令,且 Shell 命令执行的结果也无法直接输入到awk中。

7.3 执行AWK程序

  本小节中描述如何将awk程序直接写在 shell script 之中。此后使用者执行 awk 程序时,就不需要每次都键入 " awk -f program datafile"。script 中还可包含其它 Shell 命令,如此更可增加执行过程的自动化。

  建立一个简单的 awk程序 mydump.awk,如下:      

    {print}

  这个程序执行时会把数据文件的内容 print 到屏幕上( 与cat功用类似 )。print 之后未接任何参数时,表示 "print $0"。

  若欲执行该awk程序,来打印出文件 today_rpt1 及 today_rpt2 的内容时,必须于 UNIX 的命令行上执行下列命令:

  方式一

    awk -f mydump.awk today_rpt1 today_rpt2

  方式二 

    awk '{print}' today_rpt1 today_rpt2

  第二种方式系将awk 程序直接写在 Shell 的命令行上,这种方式仅适合较短的awk程序。

  方式三 建立如下的 shell script,并取名为 mydisplay,        

    awk '        # 注意以下的 awk 与 ' 之间须有空白隔开
      {print}
    ' $*    # 注意以上的 ' 与 $* 之间须有空白隔开

  执行 mydisplay 之前,须先将它改成可执行的文件(此步骤往后不再赘述)。

  请执行如下命令:          

    $ chmod +x mydisplay

  往后使用者就可直接把 mydisplay 当成指令,来display任何文件。

  例如:          

    $ ./mydisplay today_rpt1 today_rpt2

说 明:

  1. 在script文件 mydisplay 中,指令"awk"与第一个 '  之间须有空格(Shell中并无" awk' "指令)。

    第一个 ' 用以通知 Shell 其后为awk程序。

    第二个 ' 则表示 awk 程序结束。

  故awk程序中一律以"括住字符串或字符,而不使用 ' ,以免Shell混淆。

  2. $* 为 shell script中的用法,它可用来代表命令行上 "mydisplay之后的所有参数"。

  例如执行:

    $ mydisplay today_rpt1 today_rpt2

  事实上 Shell 已先把该指令转换成:

    awk '
      { print}
    '  today_rpt1 today_rpt2

  本例中,$* 用以代表 "today_rpt1 today_rpt2"。在Shell的语法中,可用 $1 代表第一个参数,$2 代表第二个参数。当不确定命令行上的参数个数时,可使用 $* 表示。

  3. awk命令行上可同时指定多个数据文件。以    

    $ awk -f dump.awk today_rpt1 today_rpt2

  为例,awk会先处理today_rpt1,再处理 today_rpt2。此时若文件无法打开,将造成错误。

  例如:不存在文件"file_no_exist",则执行:      

    $ awk -f dump.awk file_no_exit

  将产生运行时错误(无法打开文件)。

  但某些awk程序 "仅" 包含以 BEGIN 为Pattern的指令。执行这种awk程序时,awk并不须开启任何数据文件。此时命令行上若指定一个不存在的数据文件,并不会产生 "无法打开文件"的错误。(事实上awk并未打开该文件)

  例如执行:   

    $ awk 'BEGIN {print "Hello,World!!"} ' file_no_exist

  该程序中仅包含以 BEGIN 为 Pattern 的 Pattern {actions},awk 执行时并不会打开任何数据文件;所以不会因不存在文件file_no_exit而产生 " 无法打开文件"的错误。

  4. awk会将 Shell 命令行上awk程序(或 -f 程序文件名)之后的所有字符串,视为将输入awk进行处理的数据文件文件名。若执行awk的命令行上 "未指定任何数据文件文件名",则将stdin视为输入的数据来源,直到输入end of file( Ctrl-D )为止。

  读者可以用下列程序自行测试, 执行如下命令:    

    $ awk -f mydump.awk  #(未接任何数据文件文件名)

  或    

    $ ./mydisplay  #(未接任何数据文件文件名)

  将会发现:此后键入的任何数据将逐行复印一份于屏幕上。这情况不是机器当机!是因为awk程序正处于执行中。它正按程序指示,将读取数据并重新dump一次;只因执行时未指定数据文件文件名,故awk 便以stdin(键盘上的输入)为数据来源。读者可利用这个特点,设计可与awk即时聊天的程序。

7.4 改变字段的分隔符 & 用户自定义函数

  awk不仅能自动分割字段,也允许使用者改变其字段切割方式以适应各种格式的需要。使用者也可自定义函数,若有需要可将该函数单独写成一个文件,以供其它awk程序调用。

范例:承接 6.2 的例子,若八点为上班时间,请加注 "*"于迟到记录之前,并计算平均上班时间。

分析:

  1. 因八点整到达者不为迟到,故仅以到达的小时数做判断是不够的;仍应参考到达时的分钟数。若 "将到达时间转换成以分钟为单位",不仅易于判断是否迟到,同时也易于计算到达平均时间。

  2. 到达时间($2)的格式为 dd:dd 或 d:dd;数字当中含有一个 ":"。但文本数字交杂的数据awk无法直接做数学运算。(注:awk中字符串"26"与数字26 并无差异,可直接做字符串或数学运算,这是awk重要特色之一。但awk对文本数字交杂的字符串无法正确进行数学运算)。

解决的方法:

方法一   

  对到达时间($2) d:dd 或 dd:dd 进行字符串运算,分别取出到达的小时数及分钟数。

  首先判断到达小时数为一位或两位字符,再调用函数分别截取分钟数及小时数。此解法需使用下列awk字符串函数:

  length( 字符串 ):返回该字符串的长度。

  substr( 字符串,起始位置,长度):返回从起始位置起,指定长度的子字符串。若未指定长度,则返回从起始位置到字符串末尾的子字符串。

  所以:

    小时数 = substr( $2, 1, length($2) - 3 )

    分钟数 = substr( $2, length($2) - 2 )

方法二

  改变输入列字段的切割方式,使awk切割字段后分别将小时数及分钟数隔开于二个不同的字段。

  字段分隔字符 FS (field seperator) 是awk的内置变量,其默认值是空白及tab。awk每次切割字段时都会先参考FS 的内容。若把":"也当成分隔字符,则awk 便能自动把小时数及分钟数分隔成不同的字段。

  故令

    FS = "[ \t:]+"  (注:[ \t:]+ 为一Regular Expression )

  1. Regular Expression 中使用中括号 [ ... ] 表示一个字符集合,用以表示任意一个位于中括号内的字符。故可用"[ \t:]"表示 一个 空白,tab 或 ":"

  2. Regular Expression中使用 "+" 形容其前方的字符可出现一次或一次以上。

  故 "[ \t:]+" 表示由一个或多个 "空白,tab 或 : " 所组成的字符串。

  设定 FS = "[ \t:]+" 后,数据行如: "1034 7:26" 将被分割成3个字段

字段一 字段二 字段三
$1 $2 $3
1034 7 26

  明显地,awk程序中使用方法二比方法一更简洁方便。本例子中采用方法二,也借此示范改变字段切割方式的用途。

  

  编写awk程序 reformat3,如下:    

  awk '
  BEGIN {
    FS= "[ \t:]+" #改变字段切割的方式
    "date" | getline #Shell 执行 "date". getline 取得结果以$0记录
    print " Today is " ,$, $ > "today_rpt3"
    print "=========================">"today_rpt3"
    print " ID Number Arrival Time" > "today_rpt3"
    close( "today_rpt3" )
  }
  {
    #已更改字段切割方式, $2表到达小时数, $3表分钟数
    arrival = HM_to_M($, $)
    printf(" %s %s:%s %s\n", $, $, $, arrival > ? "*": " ")|"sort -k 1 >> today_rpt3"
    total += arrival
  }
  END{
    close("today_rpt3")
    close("sort -k 1 >> today_rpt3")
    printf(" Average arrival time : %d:%d\n",total/NR/, (total/NR)% ) >> "today_rpt3"
  }
  function HM_to_M( hour, min ){
    return hour* + min
  }
  ' $*

  并执行如下指令:    

    $ ./reformat3 arr.dat

  执行后,文件 today_rpt3 的内容如下:

          【译】 AWK教程指南 7AWK应用实例

说 明:

  1. awk 中也允许使用者自定义函数。函数定义方式请参考本程序,function 为 awk 的保留字。HM_to_M( ) 这函数负责将所传入的小时及分钟数转换成以分钟为单位。使用者自定函数时,还有许多细节须留心,如data scope,... ( 请参考 第十节 Recursive Program)

  2. awk中亦提供与 C 语言中相同的 Conditional Operator。上式printf()中使用arrival >480 ? "*" : " " 即为一例。若 arrival 大于 480 则return "*" ,否则return " "。

  3. % 为awk的运算符(operator),其作用与 C 语言中的 % 相同(取余数)。

  4. NR(Number of Record) 为awk的内置变量。表示awk执行该程序后所读入的记录条数。

  5. awk 中提供的 close( )指令,语法如下(有两种):

      ①  close( filename )

      ②  close( 置于pipe之前的command )

  为何本程序使用了两个 close( ) 指令:

  • 指令 close( "sort -k 1 >> today_rpt3" ),其意思为 close 程序中置于 "sort -k 1 >> today_rpt3 " 之前的 Pipe,并立刻调用 Shell 来执行"sort -k 1 >> today_rpt3"。(若未执行这指令,awk必须于结束该程序时才会进行上述动作;则这12个sort后的数据将被 append 到文件 today_rpt3 中"Average arrival time : ..." 的后方)
  • 因为 Shell 排序后的数据也要写到 today_rpt3,所以awk必须先关闭使用中的today_rpt3 以使 Shell 正确将排序后的数据追加到today_rpt3,否则2个不同的 process 同时打开一个文件进行输出将会产生不可预期的结果。

  读者应留心上述两点,才可正确控制数据输出到文件中的顺序。

  6. 指令 close("sort -k 1 >> today_rpt3")中字符串 "sort -k 1 >> today_rpt3" 必须与 pipe | 后面的 Shell Command 名称一字不差,否则awk将视为二个不同的 pipe。

  读者可于BEGIN{}中先令变量 Sys_call = "sort -k 1 >> today_rpt3",程序中再一律以 Sys_call 代替该字符串。

7.5 使用getline来读取文件数据

范例:承上题,从文件中读取当月迟到次数,并根据当日出勤状况更新迟到累计数。(按不同的月份累计于不同的文件)

分析:

  1. 程序中自动抓取系统日期的月份名称,连接上"late.dat",形成累计迟到次数的文件名称(如:Jullate.dat,...),并以变量late_file记录该文件名。

  2. 累计迟到次数的文件中的数据格式为:

       员工代号(ID) 迟到次数

  例如,执行本程序前文件 Novlate.dat 的内容为:

        【译】 AWK教程指南 7AWK应用实例

  编写程序 reformat4 如下:    

    awk '
    BEGIN {
      Sys_Sort = "sort -k 1 >> today_rpt4"
      Result = "today_rpt4"
      # 改变字段切割的方式
      # 令 Shell执行"date"; getline 读取结果,并以$0记录
      FS = "[ \t:]+"
      "date" | getline
      print " Today is " , $, $     > Result
      print "=========================" > Result
      print " ID Number Arrival Time" > Result
      close( Result )
      # 从文件按中读取迟到数据, 并用数组cnt[ ]记录. 数组cnt[ ]中以
      # 员工代号为下标, 所对应的值为该员工的迟到次数.
      late_file = $"late.dat"
      while( getline < late_file > )
        cnt[$] = $
      close( late_file )
    }
    {
      # 已更改字段切割方式, $2表小时数,$3表分钟数
      arrival = HM_to_M($, $)
      if( arrival > ){
        mark = "*"   # 若当天迟到,应再增加其迟到次数, 且令mark 为"*".
        cnt[$]++
      }
      else mark = " "
      # message 用以显示该员工的迟到累计数, 若未曾迟到message为空字符串
      message = cnt[$] ? cnt[$] " times" : ""
      printf("%s %2d:%2d %5s %s\n", $, $, $, mark, message ) | Sys_Sort
      total += arrival
    }
    END {
      close( Result )
      close( Sys_Sort )
      printf(" Average arrival time : %d:%d\n", total/NR/, (total/NR)% ) >> Result
      #将数组cnt[ ]中新的迟到数据写回文件中
      for( any in cnt )
        print any, cnt[any] > late_file
    }
    function HM_to_M( hour, min ){
      return hour* + min
    }
    ' $*

  执行后,today_rpt4 的内容如下:

              【译】 AWK教程指南 7AWK应用实例

说 明:

  1. late_file 是一变量,用以记录迟到次数的文件的文件名。late_file的值由两部分构成,前半部是当月月份名称(由调用"date"取得),后半部固定为"late.dat",如: Junlate.dat。

  2. 指令 getline < late_file 表示从late_file所代表的文件中读取一条记录,并存放于$0。若使用者可自行把数据放入$0,awk会自动对这新置入 $0 的数据进行字段分割。之后程序中可用$1, $2,..来表示该笔资料的字段一,字段二,...

  (注:有少数awk版本不容许使用者自行将数据置于 $0,遇此情况可改用gawk或nawk)

  执行getline指令时,若成功读取记录,它会返回1;若遇到文件结束,它返回0;无法打开文件则返回-1。

  3. 利用 while( getline < filename >0 ) {....}可读入文件中的每一笔数据并予处理。这是awk中用户自行读取数据文件的一个重要模式。

  4. 数组 cnt[ ] 以员工ID 当下标(index),其对应值表示其迟到的次数。

  5. 执行结束后,利用 for(Variable in array ){...}的语法  for( any in cnt ) print any, cnt[any] > late_file 将更新过的数据重新写回到记录迟到次数的文件。该语法在前面曾有说明。