使用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