好久不寫,一方面是工作原因,有些東西沒發直接發,另外的也是習慣給丟了,內因所致。今天是個好日子,走起!
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 }