Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1683099
  • 博文数量: 584
  • 博客积分: 13857
  • 博客等级: 上将
  • 技术积分: 11883
  • 用 户 组: 普通用户
  • 注册时间: 2009-12-16 09:34

分类: LINUX

2010-01-06 15:34:55

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

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

员工当日到班时间的数据文件 ( 如下列之 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. 文件中第一栏为员工代号, 第二栏为到达时间. 本范例中, 将使用该文件为数据文件.

1034 7:26
1025 7:27
1101 7:32
1006 7:45
1012 7:46
1028 7:49
1051 7:51
1029 7:57
1042 7:59
1008 8:01
1052 8:05
1005 8:12

重定向输出到文件

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", $1,$2 ) > "today_rpt1" }

执行:

$awk -f reformat1.awk arr.dat

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

ID Number Arrival Time
============================
1034 7:26
1025 7:27
1101 7:32
1006 7:45
1012 7:46
1028 7:49
1051 7:51
1029 7:57
1042 7:59
1008 8:01
1052 8:05
1005 8:12

[ 说 明 : ]

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

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

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

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

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

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

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

若采用”>>”其差异仅在第一次执行该指令时, 若已存在today_rpt1则 awk 将直接把数据append在原文件之末尾. 这一点, 与UNIX中的用法不同.

awk 中如何利用系统资源

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

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

[ 分 析 : ]

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() 二个.

在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″ 的执行次数是 “一次”.

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

使用本语法时应留心: 以上例而言,awk “立刻”调用 Shell 来执行 “ls”, 执行次数是一次.

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

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

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

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

执行如下命令:

awk -f reformat2.awk arr.dat

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

Today is  09月 21日
=========================
ID Number Arrival Time
1005 8:12
1006 7:45
1008 8:01
1012 7:46
1025 7:27
1028 7:49
1029 7:57
1034 7:26
1042 7:59
1051 7:51
1052 8:05
1101 7:32

[ 说 明 : ]

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

[ i.] Pattern { Action} 指令

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

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

awk 的输入指令 getline, 每次读取一列数据. 若getline之后

未接任何变量, 则所读入之资料将以$0 记录, 否则以所指定的变量储存之.

[ 以本例而言] :

执 行 “date” | getline 后, $0 之值为 “2007年 09月 21日 星期五 14:28:02 CST”,当 $0 之值被更新时, awk将自动更新相关的内建变量, 如: $1,$2,..,NF.故 $2 之值将为”09月”, $3之值将为”21日”.

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

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

awk提供另一个调用Shell命令的方法, 即使用awk函数system(“shell命令”)

例如:

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

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

执行 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,

#!/bin/sh

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

执行 mydisplay 之前, 须先将它改成可执行的文件(此步骤往后不再赘述). 请执行如下命令:

$ chmod +x mydisplay 

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

例如 :

$ ./mydisplay today_rpt1 today_rpt2 

[ 说 明 : ]

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

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

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

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

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

例如执行 :

$ mydisplay today_rpt1 today_rpt2 

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

awk '
{ print}
' today_rpt1 today_rpt2

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

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

以awk -f dump.awk today_rpt1 today_rpt2hf 为例,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而产生 " 无法打开文件"的错误.

awk会将 Shell 命令行上awk程序(或 -f 程序文件名)之后的所有字符串, 视为将输入awk进行处理的数据文件文件名.

若执行awk的命令行上 "未指定任何数据文件文件名", 则将stdin视为输入之数据来源, 直到输入end of file( Ctrl-D )为止.

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

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

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

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

改变 awk 切割字段的方式 & 自定义函数

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

[ 范例 : ] 承接 6.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 )

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

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

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

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

第一栏 第二栏 第三栏

$1 $2 $3

1034 7 26

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

编写awk程序 reformat3, 如下 :

#!/bin/sh

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

并执行如下指令 :

$ ./reformat3 arr.dat

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

Today is  09月 21日
=========================
ID Number Arrival Time
1005 8:12 *
1006 7:45
1008 8:01 *
1012 7:46
1025 7:27
1028 7:49
1029 7:57
1034 7:26
1042 7:59
1051 7:51
1052 8:05 *
1101 7:32
Average arrival time : 7:49

[ 说 明 : ]

awk 中亦允许使用者自定函数. 函数定义方式请参考本程序, function 为 awk 的保留字.

HM_to_M( ) 这函数负责将所传入之小时及分钟数转换成以分钟为单位. 使用者自定函数时, 还有许多细节须留心, 如data scope,.. ( 请参考 awk 之递归程序 Recursive Program)

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

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

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

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

close( filename ) #关闭普通文件

close( 置于pipe后方的command ) #关闭pipe文件,"|"后方的 command 起标志 pipe 的作用

为何本程序使用了两个 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 同时打开一个文件进行输出将会产生不可预期的结果.

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

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

读者可于BEGIN{}中先令变量 Sys_call = "sort +0n >> today_rpt3",

程序中再一律以 Sys_call 代替该字符串.

使用 getline 来读取数据

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

[ 分 析 : ]

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

累计迟到次数的文件中的数据格式为: 员工代号(ID) 迟到次数

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

1012 0
1006 1
1052 2
1034 0
1005 0
1029 2
1042 0
1051 0
1008 0
1101 0
1025 1
1028 0

编写程序 reformat4 如下:

#!/bin/sh 

awk '
BEGIN {
Sys_Sort = "sort -k 1 >> today_rpt4"
Result = "today_rpt4"

# 改变字段切割的方式
FS = "[ \t:]+"

# 令 Shell执行"date"; getline 读取结果,并以$0记录
"date" | getline
print " Today is " , $2, $3 >Result
print "=========================" > Result
print " ID Number Arrival Time" > Result
close( Result )

# 从文件按中读取迟到数据, 并用数组cnt[ ]记录. 数组cnt[ ]中以
# 员工代号为下标, 所对应的值为该员工之迟到次数.
late_file = $2"late.dat"
while( getline < late_file >0 ) cnt[$1] = $2
close( late_file )
}

{
# 已更改字段切割方式, $2表小时数,$3表分钟数
arrival = HM_to_M($2, $3)

if( arrival > 480 ){
mark = "*" # 若当天迟到,应再增加其迟到次数, 且令mark 为"*".
cnt[$1]++ }
else mark = " "

# message 用以显示该员工的迟到累计数, 若未曾迟到message为空字符串
message = cnt[$1] ? cnt[$1] " times" : ""
printf("%s %2d:%2d %5s %s\n", $1, $2, $3, mark, message ) | Sys_Sort
total += arrival
}
END {
close( Result )
close( Sys_Sort )
printf(" Average arrival time : %d:%d\n", total/NR/60, (total/NR)%60 ) >> Result

#将数组cnt[ ]中新的迟到数据写回文件中
for( any in cnt )
print any, cnt[any] > late_file
}

function HM_to_M( hour, min ){
return hour*60 + min
}
' $*

执行后, today_rpt4 之内容如下 :

Today is  09月 21日
=========================
ID Number Arrival Time
1005 8:12 * 1 times
1006 7:45 1 times
1008 8: 1 * 1 times
1012 7:46
1025 7:27 1 times
1028 7:49
1029 7:57 2 times
1034 7:26
1042 7:59
1051 7:51
1052 8: 5 * 3 times
1101 7:32
Average arrival time : 7:49

09月late.dat 文件被修改为如下:

1005 1
1012 0
1006 1
1008 1
1101 0
1025 1
1034 0
1042 0
1028 0
1029 2
1051 0
1052 3

[说 明 :]

late_file 是一变量, 用以记录迟到次数的文件的文件名.

late_file之值由两部分构成, 前半部是当月月份名称(由调用"date"取得)后半部固定为"late.dat" 如: 09月late.dat.

指令 getline < late_file 表示从late_file所代表的文件中读取一笔记录, 并存放于$0.

若使用者可自行把数据放入$0, awk会自动对这新置入 $0 的数据进行字段分割. 之后程序中可用$1, $2,..来表示该笔资料的第一栏,第二栏,..,

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

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

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

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

执行结束后, 利用 for(Variable in array ){...}的语法

for( any in cnt ) print any, cnt[any] > late_file

将更新过的迟到数据重新写回记录迟到次数的文件. 该语法在前面曾有说明.

阅读(951) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~