去年一年,我寫了將近100篇網路日誌。
現在這一年結束了,我要統計"訪問量排名",看看哪些文章最受歡迎。(隆重預告:本文結尾處將揭曉前5名。)
以往,我用的是AWStats日誌分析軟體。它可以生成很詳細的報表,但是不太容易定製,得不到某些想要的資訊。所以,我就決定自己寫一個Bash指令碼,統計伺服器的日誌,順便溫習一下指令碼知識。
事實證明,這件事比我預想的難。雖然最終指令碼只有20多行,但花了我整整一天,反覆檢視手冊,確認用法和合適的引數。下面就是我的日誌分析指令碼,雖然它還不是通用的,但是我相信裡面用到的命令,足以滿足一般的日誌分析需求,同時也是很好的學習Bash的例項。如果下面的每一個命令你都知道,我覺得可以堪稱熟練使用Bash了。
一、操作環境
在介紹指令碼之前,先講一下我的伺服器環境。
我的網路伺服器軟體是Apache,它會對每一個http請求留下記錄,就像下面這一條:
203.218.148.99 - - [01/Feb/2011:00:02:09 +0800] "GET /blog/2009/11/an_autobiography_of_yang_xianyi.html HTTP/1.1" 200 84058 "http://www.ruanyifeng.com/blog/2009/11/freenomics.html" "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13"
它的意思是2011年2月1日,IP地址為203.218.148.99的訪問者,向伺服器請求訪問網址/blog/2009/11/an_autobiography_of_yang_xianyi.html。
當天所有的訪問記錄,組成一個日誌。過去一年,一共生成了365個日誌檔案。它們存放在12個目錄中,每一個目錄表示一個月(2011-01、2011-02、......2011-12),裡面的日誌檔案依次為www-01.log、www-02.log、......www-31.log(假定該月有31天)。
在不壓縮的情況下,365個日誌檔案加起來,要佔掉10GB空間。我的目標就是分析這10GB日誌,最後得到一個如下形式的訪問量排名:
訪問量 網址1
訪問量 網址2
訪問量 網址3
...... ......
二、為什麼要用Bash
很多計算機語言,都可以用來完成這個任務。但是,如果只是簡單的日誌分析,我覺得Bash指令碼是最合適的工具。
主要原因有兩個:一是"開發快",Bash指令碼是各種Linux命令的組合,只要知道這些命令怎麼用,就可以寫指令碼,基本上不用學習新的語法,而且它不用編譯,直接執行,可以邊寫邊試,對開發非常友好。二是"功能強",Bash指令碼的設計目的,就是為了處理輸入和輸出,尤其是單行的文字,所以非常合適處理日誌檔案,各種現成的引數加上管道機制,威力無窮。
前面已經說過,最終的指令碼我只用了20多行,處理10GB的日誌,20秒左右就得到了結果。考慮到排序的巨大計算量,這樣的結果非常令人滿意,充分證明了Bash的威力。
三、總體思路
我的總體處理思路是這樣的:
第一步,處理單個日誌。統計每一天各篇文章的訪問量。
第二步,生成月度排名。將每一天的統計結果彙總,得到月度訪問量。
第三步,生成年度排名。將12個月的統計結果彙總,進行年度訪問量的排序。
四、處理單個日誌
以2011年1月1日的日誌為例,它在目錄2011-01之中,檔名是www-01.log,裡面有10萬條如下格式的記錄:
203.218.148.99 - - [01/Feb/2011:00:02:09 +0800] "GET /blog/2009/11/an_autobiography_of_yang_xianyi.html HTTP/1.1" 200 84058 "http://www.ruanyifeng.com/blog/2009/11/freenomics.html" "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13"
處理這個日誌,我只用了一行程式碼:
awk '$9 == 200 {print $7}' www-01.log | grep -i '^/blog/2011/.*\.html$' | sort | uniq -c | sed 's/^ *//g' > www-01.log.result
它用管道連線了5個命令,每一個都很簡單,我們依次來看:
(1) awk '$9 == 200 {print $7}' www-01.log
awk命令預設用空格,將每一行文字分割成若干個欄位。仔細數一下,我們需要的只是第7個欄位,即http請求的網址,{print $7}表示將第7個欄位輸出,結果就是:
/blog/2009/11/an_autobiography_of_yang_xianyi.html
考慮到我們只統計成功的請求,因此再加一個限制條件,伺服器的狀態程式碼必須是200(表示成功),寫成"$9 == 200",即第9個欄位必須是200,否則不輸出第7個欄位。
更精細的統計,還應該區分網路蜘蛛和真實訪問者,由於我想不出簡單的分辨方法,這裡只好忽略了。
(2)grep -i '^/blog/2011/.*\.html$'
在輸出的所有記錄的第7個欄位之中,並不是每一條記錄都需要統計的。根據我的文章的命名特點,它們的網址應該都以"/blog/2011/"開頭,以".html"結尾。所以,我用一個正規表示式"^/blog/2011/.*\.html$",找出這些記錄。引數i表示不區分大小寫。
(3)sort
這時,所有需要統計的記錄應該都列出來了,但是它們的次序是雜亂的。接著,使用sort命令,不過目的不是為了排序,而是把相同的網址排列在一起,為後面使用uniq命令創造條件。
(4)uniq -c
uniq的作用是過濾重複的記錄,只保留一行。c引數的作用,是在每行的開頭新增該記錄的出現次數。處理之後的輸出應該是這樣的:
32 /blog/2011/01/guidelines_for_english_translations_in_public_places.html
32 /blog/2011/01/api_for_google_s_url_shortener.html
30 /blog/2011/01/brief_history_of_arm.html
它表示以上三篇文章,在1月1日的日誌中,分別有32條、32條、30條的訪問記錄(即訪問次數)。
(5)sed 's/^ *//g' > www-01.log.result
上一步uniq命令新增的訪問次數,是有前導空格的。也就是說,在上例的32、32、30之前有一連串空格,為了後續操作的方便,這裡把前導空格刪去。sed命令是一個處理行文字的編輯器,'s/^ *//g'是一個正規表示式(^和*之間有一個空格),表示將行首的連續空格替換為空(即刪除)。接著,將排序結果重定向到檔案www-01.result。單個日誌分析就完成了。
五、月度彙總排名
經過上一步之後,1月份的31個日誌檔案,生成了31個對應的分析結果檔案。為了彙總整個月的情況,必須把這31個結果檔案合併。
(6)合併分析結果
for i in www-*.log.result
do
cat $i >> log.result
done
這是一個迴圈結構,把所有www-01.log.result形式的檔案,都寫進log.result檔案。
然後,我用一行語句,計算月度排名。
sort -k2 log.result | uniq -f1 --all-repeated=separate |./log.awk |sort -rn > final.log.result
這行語句由3個命令和1個awk指令碼組成:
(7)sort -k2 log.result
由於是31個檔案彙總,log.result檔案裡面的記錄是無序的,必須用sort命令,將相同網址的記錄歸類在一起。但是此時,訪問次數是第一個欄位,網址是第二個欄位,因此引數k2表示根據第二個欄位進行排序。
(8)uniq -f1 --all-repeated=separate
uniq的作用是過濾重複的記錄,引數f1表示忽略第一個欄位(訪問次數),只考慮後面的欄位(網址);參數列示all-repeated=separate,表示過濾掉所有隻出現一次的記錄,保留所有重複的記錄,並且每一組之間用一個空行分隔。這一步完成以後,輸出結果變成如下的形式:
617 /blog/2011/01/guidelines_for_english_translations_in_public_places.html
455 /blog/2011/01/guidelines_for_english_translations_in_public_places.html223 /blog/2011/01/2010_my_blogging_summary.html
253 /blog/2011/01/2010_my_blogging_summary.html
相同網址都歸在一組,組間用空行分割。為了簡潔,上面的例子每一組只包含兩條記錄,實際上每一組都包含31條記錄(分別代表當月每天的訪問次數)。
(9)log.awk指令碼
為了將31天的訪問次數加總,我動了很多腦筋。最後發現,唯一的方法就是用awk命令,而且必須另寫一個awk指令碼。
#!/usr/bin/awk -f
BEGIN {
RS="" #將多行記錄的分隔符定為一個空行
}{
sum=0 #定義一個表示總和的變數,初值為0
for(i=1;i<=NF;i++){ #遍歷所有欄位
if((i%2)!=0){ #判斷是否為奇數字段
sum += $i #如果是的話,累加這些欄位的值
}
}
print sum,$2 #輸出總和,後面跟上對應的網址
}
我已經對上面這個log.awk指令碼加了詳細註釋。這裡再說明幾點:首先,預設情況下,awk將"\n"作為記錄的分隔符,設定RS=""表示改為將空行作為分隔符,因此形成了一個多行記錄;其次,NF是一個awk的內建變數,表示當前行的欄位總數。由於輸入檔案之中,每一行都包含兩個欄位,第一個是訪問數,第二個是網址,所以這裡做一個條件判斷,只要是奇數字段就累加,偶數字段則一律跳過。最後,每個記錄輸出一個累加值和網址,它們之間用空格分割。
(10)sort -rn > final.log.result
對awk指令碼的處理結果進行排序,sort預設使用第一個欄位,引數r表示逆序,從大往小排;引數n表示以數值形式排序,不以預設的字典形式排序,否則會出現10小於2的結果。排序結果重定向到final.log.result。至此,月度排名完成。
六、指令碼檔案
用一個指令碼,包含上面兩節所有的內容。
#!/bin/bash
if ls ./*.result &> /dev/null #判斷當前目錄中是否有字尾名為result的檔案存在
then
rm *.result #如果有的話,刪除這些檔案
fitouch log.result #建立一個空檔案
for i in www-*.log #遍歷當前目錄中所有log檔案
do
echo $i ... #輸出一行字,表示開始處理當前檔案
awk '$9 == 200 {print $7}' $i|grep -i '^/blog/2011/.*\.html$'|sort|uniq -c|sed 's/^ *//g' > $i.result #生成當前日誌的處理結果
cat $i.result >> log.result #將處理結果追加到log.result檔案
echo $i.result finished #輸出一行字,表示結束處理當前檔案
doneecho final.log.result ... #輸出一行字,表示最終統計開始
sort -k2 log.result | uniq -f1 --all-repeated=separate |./log.awk |sort -rn > final.log.result #生成最終的結果檔案final.log.result
echo final.log.result finished #輸出一行字,表示最終統計結束
這就是月度排名的最終指令碼。編寫的時候,我假定這個指令碼和log.awk指令碼與日誌檔案在同一個目錄中,而且這兩個指令碼都具有執行許可權。
年度排名的處理與此類似,就不再贅述了。
=================================================================
關於指令碼介紹,就到此為止。
接下來,揭曉2011年度訪問量最大的我的5篇文章。
我真想問問Google Adsense中國小組的成員:"難道你們都是機器人嗎?難道你們看不出來哪些是流氓網站,哪些是正派網站嗎?你們是否真的盡職工作了,還是在不負責任地草菅人命?"
第四名、《賈伯斯的告別》
斯蒂夫·賈伯斯活著的時候,對病情諱莫如深,外界對他的身體狀態毫不知情。現在他去世了,根據各方面透露的資訊,我們終於可以還原他的病歷,瞭解像他這樣偉人怎樣對待生與死。
在此之前,他幾乎沒有打過高爾夫球,甚至對這項運動都沒有太大興趣。他的計劃是,辭職以後,每天練習6個小時,一週練習6天,堅持6年,總計超過10000個小時,然後成為職業選手。他把這稱為"Dan計劃"。
第二名、《保持簡單----紀念丹尼斯•裡奇(Dennis Ritchie)》
13歲的丹尼斯•裡奇(Dennis Ritchie),就這樣隨著父親一起來到新澤西。那時,誰也沒有想到,這個文靜的少年將在這裡待上一輩子,並且創造出改變世界的發明。
第一名、《人生只有900個月》
你可以畫一個30x30的表格,一張A4紙就夠了。每過一個月,就在一個格子裡打鉤。你全部的人生就在這張紙上。你會因此有一個清晰的概念:你的人生是如何蹉跎的。
(完)