[TOC] # 数据转换与提炼 > awk其中一个最为常用的作用就是格式转换处理,通常源数据来自于某个程序产生的输出结果信息,这些数据需要通过`awk`进行格式转换处理然后提供给另外的程序进行操作。 > 另外一个应用是从一个比较大的数据集合当中提取出相关的数据进行处理或分析,通常提取数据过程中会进行格式化处理以及信息汇总等处理。 > 本节包含一系列相关示例来帮助大家更加熟练的编写`Awk`脚本。 ## 示例: 汇总各列数据之和 >前面我们简单使用过`awk`的命令行求和方法,接下来我们以这个示例来编写一个稍微复杂一些的求和示例. 功能说明: 对每列的数值进行求和,最后输出每列的总和信息 输入数据: M行N列的数字 输出数据: N列数字(每列数字之和),每列用`\t`分隔符分开 举例: ``` 输入数据: 1 2 3 2 2 2 3 3 3 输出数据: 6 7 8 ``` 以下是`Awk`代码示例: ``` # sum1 - 对每列的数值进行求和,最后输出每列的总和信息 { for(i = 1; i<= NF; i++) sum[i] += $i if( NF > max_fd) max_fd = NF }END{ for( i = 1; i < max_fd ; i++) printf("%g\t", sum[i]) printf("%g\n", sum[max_fd]); } ``` ``` # sum2 过滤掉非数字列的求和 NR=1{ nf = NF; for(i = 1; i <= nf; i++) numcol[i] = isnum($i) } { for(i = 1; i <= NF; i++) if(numcol[i]) sum[i] += $i } END{ for( i = 1; i <= nf ; i++) { if(numcol[i]) printf("%g" , sum[i]) else printf("--") printf(i < nf ? "\t" : "\n") } } function isnum(n){ return n~/^[+-]?[0-9]+$/ } ``` 思考问题: 1. 本例考虑了可能中间某个字段为空情况,这样导致解析字段顺序错位如“1\t\t2”,本来应该是三列,由于第二列为空就被解析为两列,考虑下设置`FS`试试怎么样(或者`-F'\t'`)? 2. 为数据处理忽略空白行? 3. 为数字识别表达式增加更全的识别规则(如小数)? ## 示例:计算百分比和数量 > 最近得到了一批学生考试成绩,我们想要通过Awk命令脚本对这些成绩进行以下分析,看看在每个分数段的学生人数占比分布情况。 下面我们来看看实现方法: ``` $ cat histgram #!/usr/bin/awk -f # histgram # input : [0-100]数字 # output: 分数分布图 /^[0-9]+$/{ x[ int($1/10) ]++ ; total++} $1>=60{ pass++} END{ for(i = 0 ; i < 10; i++) printf(" %2d - %2d: %3d %s\n", 10*i , 10*i+9, x[i], rep(x[i],"+")) printf("100: %3d %s\n", x[10], rep(x[10],"+")) printf("total: %3d ,pass: %3d, pecent: %4.02f%\n", total , pass , pass/total*100); } function rep(n, s, t){ while( n-- > 0) t = t s return t } $ head data 70 85 61 90 51 26 36 4 22 23 #执行方法: $ ./histgram data 0 - 9: 17 +++++++++++++++++ 10 - 19: 15 +++++++++++++++ 20 - 29: 23 +++++++++++++++++++++++ 30 - 39: 18 ++++++++++++++++++ 40 - 49: 23 +++++++++++++++++++++++ 50 - 59: 18 ++++++++++++++++++ 60 - 69: 21 +++++++++++++++++++++ 70 - 79: 24 ++++++++++++++++++++++++ 80 - 89: 14 ++++++++++++++ 90 - 99: 20 ++++++++++++++++++++ 100: 12 ++++++++++++ total: 205 ,pass: 91, pecent: 44.39% ``` 通过这样的分析,我们能够直观的看到分数排布情况和及各的人数占比。 思考问题: 1. 如何避免因为输入数据量过大引起的输出"+"号过长问题? ## 示例: 随机数的生成方法 > 为了配合上面的示例,我们编写个随机数生成脚本命令,这样我们可以随即生成一些成绩数了。 ``` $ cat rand.awk #!/usr/bin/awk -f ## 功能: 输出数值数列 ## 参数: r c, r c m , ## 输出: 输出 r行 , c列随机数 ,最大值为 m(默认100) function usage(f){ printf("Usage: %s r c\n", f); printf(" %s r c m\n", f); printf("其中 r :行数, c :列数 , m :最大数值,默认100 \n"); } BEGIN{ if( ARGC == 3 ){r = ARGV[1] ; c = ARGV[2] ; m = 100} else if( ARGC == 4 ){r = ARGV[1] ; c = ARGV[2] ; m = ARGV[3] } else { usage(ARGV[0]) exit 1 } "echo $RANDOM" | getline sr srand(sr) ## 设置随机数种子,避免生成相同序列 for( i = 0; i< r ; i ++) { for(j = 0 ; j < c -1; j++) printf("%d ", rand()*m); printf("%d\n", rand()*m); } } ``` 思考问题: 1. 我们已经介绍过srand()函数了,你应该知道为什么使用`sr`随即值了吧? ## 示例: 逗号表达形式数字处理方法 > 逗号数字表达形式时,我们需要将逗号去掉了才能进行数值计算,下面这个例子就是这样的功能。 下面这个例子用来对逗号表达形式数值进行求和计算: ``` $ cat sumcomma.awk #!/usr/bin/awk -f # sumcomma.awk - 计算使用逗号表达数字的总和 # 例如 123,456.78 { gsub(",",""); sum += $0 } END{ print sum} ``` 我们看到这个脚本使用gsub()函数完成的替换逗号功能,但是大家也都想到了一个隐患,就是没有判断逗号所在位置是否正确的问题,也就是缺少了数字格式校验。 下面示例功能是将数字修改成逗号表达格式: ``` $ cat addcomma.awk #!/usr/bin/awk -f # addcomma - 按照逗号格式表达数字 # input: 数字 # output:逗号格式表达数字 # eg: rand.awk 30 1 99900000000 | ./addcomma.awk { printf("%-12s %20s\n", $0, addcomma($0))} function addcomma(x, num) { if( x < 0 ) return "-" addcomma(-x) num = sprintf("%.2f", x) # num is dddddd.dd while( num ~/[0-9][0-9][0-9][0-9]/) sub(/[0-9][0-9][0-9][,.]/, ",&", num) return num } ``` 思考问题: 1. 参考`addcomma.awk`方法给`sumcomma.awk`增加一个逗号表达格式检验规则? ## 示例: 固定长度数据的格式转换(日期格式) > 数据处理时,你会常常看到需要对某种定长格式数据进行格式转换,例如: 将日期格式mmddyy转换为yymmdd格式。 下面就是定长日期格式转换的示例: ``` $ cat dateconvert.awk #!/usr/bin/awk -f # dateconvert.awk # 日期格式转换: 将第一列转换 mmddyy ==> yymmdd { $1=substr($1,5,2) substr($1,1,2) substr($1,3,2) print } ``` 日期格式转换后,我们就可以对数据按年进行排序了,也可以提供给其他程序作为输入数据了。 思考问题: 1. 给这个转换功能的日期格式增加一个有效性验证? 2. 如何实现一个将日期转换为天数的函数功能,转换后的结果可以用来对两个日期进行大小比较? ## 示例: 程序交叉引用检查工具 > Awk命令常用于提取其他程序输出的信息,有时候输出信息是一些同类信息行(字段分隔或substr处理足以应付), 有时候上游输出信息是提供给人看的(非固定格式),这时Awk程序要做的是针对不同格式进行仔细的处理, 以便从不相关的信息中提取信息。 二进制可执行程序或者函数库文件通常都是通过很多文件编译链接生成的。每个文件中可以定义不同类别的功能函数,这样既方便函数分类也方便重复使用。 Unix和Linux系统中都有`nm`命令帮助我们显示输出对象文件中的符号信息,下面就是`/usr/lib64/libelf.a`文件的符号信息: ``` $ nm /usr/lib64/libelf.a |head elf_version.o: 0000000000000000 T elf_version U _GLOBAL_OFFSET_TABLE_ U __libelf_seterrno 0000000000000000 D __libelf_version 0000000000000004 C __libelf_version_initialized elf_hash.o: 0000000000000000 T elf_hash ``` 只有一列的行是对象文件名,两列的行为使用函数名称,三列的行是此文件定义的函数名称,T表示定义的是函数,U表示这个名称是没有定义在此文件中。 ``` # nm.format - 为nm输出信息增加文件名称 NF==1 { file = $1 } NF==2 { print file,$1,$2} NF==3 { print file,$2,$3} ``` 当然,这是一个简单示例,实际上`nm`命令是可以通过参数增加文件名称以及行号信息等等。 ## 示例:格式化输出支票信息 > 接下来我们来看一个格式化输出信息的示例,打印费用账单。 我们假设输入信息有三列,格式为: `流水号\t费用合计\t付费人` 输出格式如下: ``` 12345 10月 08日,16:44:59 Pay to Jim------------------------------------------ $1020.05 the sum of one thousand twenty dollars and 5 cents exactly ``` 来看看具体实现的代码: ``` #!/usr/bin/awk -f ## prchecks.awk # input: number \t amount \t payee # output: 八行文本信息 BEGIN{ FS="\t" dashes = sp45 = sprintf("%45s", " ") gsub(/ /,"-",dashes) "date"|getline date split(date, d, " ") date = d[2] " " d[3] "," d[5] initnum() } NF != 3 || $2 >= 1000000 { printf("\nline %d illegal:\n%s\n\nVOID\nVOID\n\n\n",NR,$0) next } { printf("\n") printf("%s%s\n", sp45, $1) printf("%s%s\n", sp45, date) amt = sprintf("%.2f", $2) printf("Pay to %45.45s $%s\n",$3 dashes, amt) printf("the sum of %s\n", numtowords(amt)) printf("\n\n\n") } function numtowords(n, cents, dols){ cents = substr(n, length(n)-1,2) + 0 dols = substr(n,1,length(n)-3) +0 if(dols == 0) return "zero dollars and " cents " cents exactly" return intowords(dols) " dollars and " cents " cents exactly" } function intowords(n) { n = int(n) if(n >= 1000) return intowords(n/1000) " thousand " intowords(n%1000) if(n >= 100) return intowords(n/100) " hundred " intowords(n%100) if(n >= 10) return tens[int(n/10)] " " intowords(n%10) return nums[n] } function initnum(){ split("one two three four five six seven eight nine "\ "ten eleven twelve thirteen fourteen fifteen "\ "sixteen seventeen eighteen nineteen", nums, " ") split("ten twenty thirty forty fifty sixty "\ "seventy eighty ninety", tens, " ") } ```