1. 程式人生 > >使用shell+awk完成Hive查詢結果格式化輸出

使用shell+awk完成Hive查詢結果格式化輸出

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

btw,實際上這種格式化輸出應該不只限於某一種需求,差不多是通用的。

需求

--基本的:當前Hive查詢結果存在資料與表頭無法對齊的情況,不便於監控人員直接檢視,或者匯出到excel中,需要提供一個指令碼,將查詢結果處理下,便於後續的檢視或者操作。

--額外的:A、每次查詢出來的結果欄位數、欄位長度不固定;B、每個資料檔案中可能包含不只一套查詢結果,即存在多個schema。

想法

對於基本需求而言,無非就是將資料檔案用格式化輸出整理一下,直接想到了awk。

對於補充的情況,A:需要實現一種機制,基於資料檔案,動態地確定格式化輸出的引數:欄位個數,以及每個格式化字串的長度引數;B:實現對資料檔案根據欄位數切割成多段,然後對於每段資料套用前面的指令碼處理。

做法

基本需求:

1、指定欄位分隔符為“\t”

2、將每個欄位按照指定長度格式化輸出

BEGIN{
FS="\t"
}
{
printf "%-"len"s\t",$i
}

額外需求A:

需要把程式碼寫成“活”的,適應各種不同的資料檔案,如前面所說,實際上就是在執行格式化輸出之前,將資料檔案掃描一遍,用一個數組記錄下檔案中每個欄位的max length,然後將這個max length作為該檔案內格式化輸出的額定寬度。

1、初始化一個fieldLen陣列

2、掃描整個檔案,更新fieldLen陣列

3、將fieldLen陣列,用於格式化輸出

BEGIN{
FS="\t"
}
NR==1{
for (i=1;i<=NF;i++)
        fieldLen[i]=0
}
{

for (i=1;i<=NF;i++)
{
        len=length($i)
        
        if (len>fieldLen[i])
        {
                fieldLen[i]=len        
        }
}

}

END{
for (i=1;i<=NF;i++)
{
        printf "%-s",fieldLen[i]
        if (i<NF)
                printf "\t"
        else
                printf "\n"

}
}

這裡要注意的是,fieldLen的初始化要在NR==1的時候,在BEGIN裡面,NF為0

額外需求B:

這裡需要一些臨時變數,標記分割出來的資料塊分支:suffix標記不同的分支,fields當前處理資料塊的欄位數

處理過程根據前面的臨時變數,完成資料檔案分割。此處有一個侷限在於,對於檔案內的多個數據分塊,只能處理“AAABBBCCC”這樣,同一類資料放在一起的,指令碼會分成3塊;而對於“AABCABBCC”這種的,則會分割成6塊。

BEGIN{
FS="\t"            
suffix=0        
filename=ARGV[1]    
fields=0        
}
{
if (NF!=fields)
{
    fields=NF
    suffix+=1
}
print $0>filename"."suffix
}
END{
print suffix        
}

基本的思路,就如上面所示。

但是,完成上面的部分,可能不到一半的工作量,接下來,說幾個比較麻煩的問題:

A、漢字的問題

這個也是對不齊的主要原因。

在putty裡面顯示的時候,一個漢字佔2個字寬,一個ASCII字元佔一個字寬。但是,在呼叫awk內建的length()函式時,一個漢字跟一個ASCII字元長度是一樣的。所以為了在putty上看到的內容是對齊的,需要在格式化輸出的時候,對fieldLen的值進行修正。

例子如:

舉例

如上,計算得到的fieldLen為4,但實際上需要8;但是在printf的時候,為了對齊,從“abs”到“泰國香蕉”printf的len值是不一樣的,根據欄位情況,動態決定

所以需要修正的有2處:

1、在計算fieldLen的時候,根據漢字情況,將length($i)獲取值加上一個變數

for (i=1;i<=NF;i++)
{
    len=length($i)
        for (j=1;j<=length($i);j++)
        if (substr($i,j,1) > "\177")
            len+=1 
        if (len>fieldLen[i])
        {
                fieldLen[i]=len        
        }
}

2、在printf格式化輸出的時候,根據漢字情況,給fieldLen[i]減去一個變數
for (i=1;i<=NF;i++)
{

    len=0
    for (j=1;j<=length($i);j++)
        if (substr($i,j,1) > "\177")
            len+=1
    printf "%-'"fieldLen[i]-len"'s",$i
        
    if (i<NF)
        printf "\t"
    else
        printf "\n"
}

原理比較簡單了,就是前面提到的,漢字比ASCII字元多佔一個位置,所以在獲取fiedlLen的時候,要加上漢字多佔的部分;在格式化輸出的時候,漢字要減去多佔的部分。

這裡用到了一種awk內識別漢字的方法,參考了網上一個同學的帖子:

 for (j=1;j<=length($i);j++)
         if (substr($i,j,1) > "\177")
              #TODO

原理就是挨個字元進行檢測,“\177”是8進位制的127,超過127的都算漢字。

B、多檔案輸入的問題

按照前面的思路,先要掃描一遍,將資料檔案的欄位資訊存下來,然後再引入欄位資訊和資料檔案,做最終的處理。

這裡有一個問題是:是否有必要將欄位資訊儲存成單獨檔案?從awk的原理來看,基本上是一遍掃描,當第一遍掃描完,之後,遊標已經到了檔案末尾。這樣看不太方便在一個awk處理流程中完成對同一個檔案的2次掃描。即使有方法,或許也比較複雜,2遍就兩遍吧。

awk多檔案輸入比較簡單,但是我們這裡的需求是先讀取第一個檔案的內容,儲存到fieldLen陣列;然後利用fieldLen陣列,處理第二個檔案。這裡用到的是NR,FNR這兩個變數的作用域不同而完成的:NR服務於整個awk處理,FNR服務於某個檔案。

NR==FNR{
for (i=1;i<=NF;i++)
    fieldLen[i]=$i
}
NR!=FNR{
      #TODO      
}

C、printf變數做字寬的問題

前面一直說,根據資料檔案,動態地確定欄位寬度,所以到最後一步,格式化輸出的時候,%s在指定寬度的時候,需要用一個變數指定寬度。這是一個awk語言瞭解是否透徹的問題,花費了不短時間才搞定,直接貼程式碼吧。

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!

最後附上程式碼

#!/bin/sh

if [ -f $1.txt ];then            
    rm $1.txt
fi

branch=`awk -f split.awk $1`        

for ((i=1;i<=$branch;i++));do        

current=$1.$i
                    
awk '
BEGIN{
FS="\t"
}
NR==1{
for (i=1;i<=NF;i++)
        fieldLen[i]=0
}
{

for (i=1;i<=NF;i++)
{
    len=length($i)
        for (j=1;j<=length($i);j++)
        if (substr($i,j,1) > "\177")
            len+=1 
        if (len>fieldLen[i])
        {
                fieldLen[i]=len        
        }
}

}

END{
for (i=1;i<=NF;i++)
{
        printf "%-s",fieldLen[i]
    if (i<NF)
                printf "\t"
        else
                printf "\n"

}
}
' $current > $current.schema


awk -f execFormat.awk $current.schema $current > $current.txt

rm $current
rm $current.schema

done

for ((i=1;i<=$branch;i++));do

current=$1.$i.txt

cat $current >> $1.txt            

rm $current

done

format.sh
#!/usr/bin/awk
BEGIN{
FS="\t"        
suffix=0        
filename=ARGV[1]    
fields=0        
}
{
if (NF!=fields)
{
    fields=NF
    suffix+=1
}
print $0>filename"."suffix
}
END{
print suffix        
}

split.awk
#!/usr/bin/awk
BEGIN{
FS="\t" 
}
NR==FNR{
for (i=1;i<=NF;i++)
    fieldLen[i]=$i
}
NR!=FNR{

for (i=1;i<=NF;i++)
{
    len=0
    for (j=1;j<=length($i);j++)
        if (substr($i,j,1) > "\177")
            len+=1
    printf "%-'"fieldLen[i]-len"'s",$i
        
    if (i<NF)
        printf "\t"
    else
        printf "\n"
}
}

execFormat.awk

來源:http://www.cnblogs.com/YFYkuner/p/3739083.html