處理Apache日誌的Bash指令碼

阮一峰發表於2012-01-06

去年一年,我寫了將近100篇網路日誌。

現在這一年結束了,我要統計"訪問量排名",看看哪些文章最受歡迎。(隆重預告:本文結尾處將揭曉前5名。)

處理Apache日誌的Bash指令碼

以往,我用的是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.html

  223 /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 #如果有的話,刪除這些檔案
  fi

  touch 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 #輸出一行字,表示結束處理當前檔案
  done

  echo 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帳戶被關》

處理Apache日誌的Bash指令碼

我真想問問Google Adsense中國小組的成員:"難道你們都是機器人嗎?難道你們看不出來哪些是流氓網站,哪些是正派網站嗎?你們是否真的盡職工作了,還是在不負責任地草菅人命?"

第四名、《賈伯斯的告別》

處理Apache日誌的Bash指令碼

斯蒂夫·賈伯斯活著的時候,對病情諱莫如深,外界對他的身體狀態毫不知情。現在他去世了,根據各方面透露的資訊,我們終於可以還原他的病歷,瞭解像他這樣偉人怎樣對待生與死。

第三名、《Dan計劃:重新定義人生的10000個小時》

處理Apache日誌的Bash指令碼

在此之前,他幾乎沒有打過高爾夫球,甚至對這項運動都沒有太大興趣。他的計劃是,辭職以後,每天練習6個小時,一週練習6天,堅持6年,總計超過10000個小時,然後成為職業選手。他把這稱為"Dan計劃"。

第二名、《保持簡單----紀念丹尼斯•裡奇(Dennis Ritchie)》

處理Apache日誌的Bash指令碼

13歲的丹尼斯•裡奇(Dennis Ritchie),就這樣隨著父親一起來到新澤西。那時,誰也沒有想到,這個文靜的少年將在這裡待上一輩子,並且創造出改變世界的發明。

第一名、《人生只有900個月》

處理Apache日誌的Bash指令碼

你可以畫一個30x30的表格,一張A4紙就夠了。每過一個月,就在一個格子裡打鉤。你全部的人生就在這張紙上。你會因此有一個清晰的概念:你的人生是如何蹉跎的。

(完)

相關文章