使用shell+awk完成Hive查询结果格式化输出
好久不写,一方面是工作原因,有些东西没发直接发,另外的也是习惯给丢了,内因所致。今天是个好日子,走起!
btw,实际上这种格式化输出应该不只限于某一种需求,差不多是通用的。
需求:
--基本的:当前Hive查询结果存在数据与表头无法对齐的情况,不便于监控人员直接查看,或者导出到excel中,需要提供一个脚本,将查询结果处理下,便于后续的查看或者操作。
--额外的:A、每次查询出来的结果字段数、字段长度不固定;B、每个数据文件中可能包含不只一套查询结果,即存在多个schema。
想法:
对于基本需求而言,无非就是将数据文件用格式化输出整理一下,直接想到了awk。
对于补充的情况,A:需要实现一种机制,基于数据文件,动态地确定格式化输出的参数:字段个数,以及每个格式化字符串的长度参数;B:实现对数据文件根据字段数切割成多段,然后对于每段数据套用前面的脚本处理。
做法:
基本需求:
1、指定字段分隔符为“\t”
2、将每个字段按照指定长度格式化输出
1 BEGIN{ 2 FS="\t" 3 } 4 { 5 printf "%-"len"s\t",$i 6 }
额外需求A:
需要把代码写成“活”的,适应各种不同的数据文件,如前面所说,实际上就是在执行格式化输出之前,将数据文件扫描一遍,用一个数组记录下文件中每个字段的max length,然后将这个max length作为该文件内格式化输出的额定宽度。
1、初始化一个fieldLen数组
2、扫描整个文件,更新fieldLen数组
3、将fieldLen数组,用于格式化输出
1 BEGIN{ 2 FS="\t" 3 } 4 NR==1{ 5 for (i=1;i<=NF;i++) 6 fieldLen[i]=0 7 } 8 { 9 10 for (i=1;i<=NF;i++) 11 { 12 len=length($i) 13 14 if (len>fieldLen[i]) 15 { 16 fieldLen[i]=len 17 } 18 } 19 20 } 21 22 END{ 23 for (i=1;i<=NF;i++) 24 { 25 printf "%-s",fieldLen[i] 26 if (i<NF) 27 printf "\t" 28 else 29 printf "\n" 30 31 } 32 }
这里要注意的是,fieldLen的初始化要在NR==1的时候,在BEGIN里面,NF为0
额外需求B:
这里需要一些临时变量,标记分割出来的数据块分支:suffix标记不同的分支,fields当前处理数据块的字段数
处理过程根据前面的临时变量,完成数据文件分割。此处有一个局限在于,对于文件内的多个数据分块,只能处理“AAABBBCCC”这样,同一类数据放在一起的,脚本会分成3块;而对于“AABCABBCC”这种的,则会分割成6块。
1 BEGIN{ 2 FS="\t" 3 suffix=0 4 filename=ARGV[1] 5 fields=0 6 } 7 { 8 if (NF!=fields) 9 { 10 fields=NF 11 suffix+=1 12 } 13 print $0>filename"."suffix 14 } 15 END{ 16 print suffix 17 }
基本的思路,就如上面所示。
但是,完成上面的部分,可能不到一半的工作量,接下来,说几个比较麻烦的问题:
A、汉字的问题
这个也是对不齐的主要原因。
在putty里面显示的时候,一个汉字占2个字宽,一个ASCII字符占一个字宽。但是,在调用awk内置的length()函数时,一个汉字跟一个ASCII字符长度是一样的。所以为了在putty上看到的内容是对齐的,需要在格式化输出的时候,对fieldLen的值进行修正。
例子如:
如上,计算得到的fieldLen为4,但实际上需要8;但是在printf的时候,为了对齐,从“abs”到“泰国香蕉”printf的len值是不一样的,根据字段情况,动态决定
所以需要修正的有2处:
1、在计算fieldLen的时候,根据汉字情况,将length($i)获取值加上一个变量
1 for (i=1;i<=NF;i++) 2 { 3 len=length($i) 4 for (j=1;j<=length($i);j++) 5 if (substr($i,j,1) > "\177") 6 len+=1 7 if (len>fieldLen[i]) 8 { 9 fieldLen[i]=len 10 } 11 }
2、在printf格式化输出的时候,根据汉字情况,给fieldLen[i]减去一个变量
1 for (i=1;i<=NF;i++) 2 { 3 4 len=0 5 for (j=1;j<=length($i);j++) 6 if (substr($i,j,1) > "\177") 7 len+=1 8 printf "%-'"fieldLen[i]-len"'s",$i 9 10 if (i<NF) 11 printf "\t" 12 else 13 printf "\n" 14 }
原理比较简单了,就是前面提到的,汉字比ASCII字符多占一个位置,所以在获取fiedlLen的时候,要加上汉字多占的部分;在格式化输出的时候,汉字要减去多占的部分。
这里用到了一种awk内识别汉字的方法,参考了网上一个同学的帖子:
1 for (j=1;j<=length($i);j++) 2 if (substr($i,j,1) > "\177") 3 #TODO
原理就是挨个字符进行检测,“\177”是8进制的127,超过127的都算汉字。
B、多文件输入的问题
按照前面的思路,先要扫描一遍,将数据文件的字段信息存下来,然后再引入字段信息和数据文件,做最终的处理。
这里有一个问题是:是否有必要将字段信息保存成单独文件?从awk的原理来看,基本上是一遍扫描,当第一遍扫描完,之后,游标已经到了文件末尾。这样看不太方便在一个awk处理流程中完成对同一个文件的2次扫描。即使有方法,或许也比较复杂,2遍就两遍吧。
awk多文件输入比较简单,但是我们这里的需求是先读取第一个文件的内容,保存到fieldLen数组;然后利用fieldLen数组,处理第二个文件。这里用到的是NR,FNR这两个变量的作用域不同而完成的:NR服务于整个awk处理,FNR服务于某个文件。
1 NR==FNR{ 2 for (i=1;i<=NF;i++) 3 fieldLen[i]=$i 4 } 5 NR!=FNR{ 6 #TODO 7 }
C、printf变量做字宽的问题
前面一直说,根据数据文件,动态地确定字段宽度,所以到最后一步,格式化输出的时候,%s在指定宽度的时候,需要用一个变量指定宽度。这是一个awk语言了解是否透彻的问题,花费了不短时间才搞定,直接贴代码吧。
1 printf "%-'"fieldLen[i]-len"'s",$i
D、效率的问题
在脚本执行过程中,出于了处理方便或者逻辑明确的考虑,存在不少的写文件操作。特做如下的测试:
文件 | 记录数 | size | 处理时间 |
a.dat | 642 |
240K |
<1s |
b.dat |
500000 |
30M |
35s |
c.dat |
1000000 |
168M |
3min42s |
combine.dat |
1500642 |
198M |
4min9s |
从实际角度来说,这种格式化的处理,通常数据量不会特别大,同时对实时性要求不那么高。所以够用就行,暂时可以接受。后续在做改进吧。
Over!
最后附上代码
1 #!/bin/sh 2 3 if [ -f $1.txt ];then 4 rm $1.txt 5 fi 6 7 branch=`awk -f split.awk $1` 8 9 for ((i=1;i<=$branch;i++));do 10 11 current=$1.$i 12 13 awk ' 14 BEGIN{ 15 FS="\t" 16 } 17 NR==1{ 18 for (i=1;i<=NF;i++) 19 fieldLen[i]=0 20 } 21 { 22 23 for (i=1;i<=NF;i++) 24 { 25 len=length($i) 26 for (j=1;j<=length($i);j++) 27 if (substr($i,j,1) > "\177") 28 len+=1 29 if (len>fieldLen[i]) 30 { 31 fieldLen[i]=len 32 } 33 } 34 35 } 36 37 END{ 38 for (i=1;i<=NF;i++) 39 { 40 printf "%-s",fieldLen[i] 41 if (i<NF) 42 printf "\t" 43 else 44 printf "\n" 45 46 } 47 } 48 ' $current > $current.schema 49 50 51 awk -f execFormat.awk $current.schema $current > $current.txt 52 53 rm $current 54 rm $current.schema 55 56 done 57 58 for ((i=1;i<=$branch;i++));do 59 60 current=$1.$i.txt 61 62 cat $current >> $1.txt 63 64 rm $current 65 66 done
1 #!/usr/bin/awk 2 BEGIN{ 3 FS="\t" 4 suffix=0 5 filename=ARGV[1] 6 fields=0 7 } 8 { 9 if (NF!=fields) 10 { 11 fields=NF 12 suffix+=1 13 } 14 print $0>filename"."suffix 15 } 16 END{ 17 print suffix 18 }
1 #!/usr/bin/awk 2 BEGIN{ 3 FS="\t" 4 } 5 NR==FNR{ 6 for (i=1;i<=NF;i++) 7 fieldLen[i]=$i 8 } 9 NR!=FNR{ 10 11 for (i=1;i<=NF;i++) 12 { 13 len=0 14 for (j=1;j<=length($i);j++) 15 if (substr($i,j,1) > "\177") 16 len+=1 17 printf "%-'"fieldLen[i]-len"'s",$i 18 19 if (i<NF) 20 printf "\t" 21 else 22 printf "\n" 23 } 24 }