使用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
format.sh
 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 }
split.awk
 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 }
execFormat.awk

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM