- 論壇徽章:
- 0
|
謹(jǐn)寫此篇希望為 Ruby 和 CU Ruby 版的發(fā)展盡一份薄力。因?yàn)?Ruby 真的是很優(yōu)雅、很強(qiáng)大而又很靈活的語言。
前言
開始知道 Ruby 是什么時(shí)候已經(jīng)忘了,但是確實(shí)一直深深被其所吸引。用 PHP 差不多 3 年了,卻一直不喜歡它過于隨意的方式,最不喜歡的是把所有函數(shù)都放在全局空間,名字和函數(shù)位置也很讓人詬病。但是一直沒有一個(gè)特別的需求一定要學(xué) Ruby,所以也就隨意看了些 Ruby 的教程,后來看到 RoR,很受啟發(fā),用 PHP 5 的新功能寫了一個(gè)類似的小框架。但還是沒有真正學(xué) Ruby,只覺得,哦,方便,哦,清楚,哦,不錯(cuò),就完了。
終于,這一天到來了。這次學(xué)習(xí) System Adminstration,第一個(gè)作業(yè),寫一個(gè)分析訪問記錄(log)的小程序,要求,用腳本(當(dāng)然你想用 C++ 沒人攔你)。而作業(yè)要求明明寫到,Python/Perl/Ruby。Python 最近也在看,因?yàn)?Ruby 太常拿來給 Python 比較了。但是就 Python 來說,OO 得不夠徹底,雖然在很多情況下更為清晰(比如 list comprehension),但是對(duì)于我這種完美主義者來說,語言的 consistency 比較重要,所以沒有更深地接觸 Python。Perl 就不說了,就我個(gè)人來說不愿意花太多時(shí)間去記一門語言的語法和關(guān)鍵詞等等,因?yàn)橐呀?jīng)要花很多時(shí)間來記 Unix 和 Emacs 的命令什么的了。只想輕松一點(diǎn)。
后來去問講師,可以用 PHP 么?我其實(shí)覺得應(yīng)該是可以的,畢竟都是 Scripting Language,某種意義上。但是他明確回答不行。自然最后就只有 Ruby 了。
開始
我先說明:我還算比較了解 PHP 關(guān)于中小型網(wǎng)站開發(fā)的部分,PHP 5 也有接觸。另外 C++/Java 都會(huì)一些;關(guān)于 Ruby 的資料看得不多也不系統(tǒng),很多時(shí)候就是蜻蜓點(diǎn)水類地泛泛而看。
開發(fā)環(huán)境
這是一個(gè)很小的程序,所以不太需要太強(qiáng)大的 IDE,EditPlus 足夠了,但是我想要更融合 Ruby 的編輯器,所以搜索了之后選定 RDE,很簡(jiǎn)單,也很小。下載 RDE 之后,你需要選擇 Ruby 的位置,然后即可進(jìn)入。
作業(yè)
這次的作業(yè)也很簡(jiǎn)單,要求是:
- 1183245989.174 0.129 137.157.56.174 200 1277 GET http://linux.pacific.net.au/linux/packman/suse/10.2/repodata/repomd.xml "text/xml"
- 1183245989.254 0.059 137.157.56.174 200 1277 GET http://linux.pacific.net.au/linux/packman/suse/10.2/repodata/repomd.xml "text/xml"
- ...
復(fù)制代碼
分析類似于上面這種 log 文件。需要的東西是:
C3 = User IP (Identification of User)
C4 = HTTP Status (200 for success)
C5 = File size sent
C6 = HTTP Request
C7 = Host (extract from URL)
C8 = MIME quoted in doublequotes
程序命令為:
logwhacker [-u ip-address] [-s] [-b] [-t] logfile
-u 為用戶檢查模式,輸出 IP 為 ip-address 的用戶有
- 多少行記錄
- 多少 GET
- 多少成功的 GET
- 下載了多少 Byte
- 最常去的域名/主機(jī)名
- 最常下載的 MIME
-s 為報(bào)告模式,輸出
- 總共多少行記錄
- 總共傳送多少 Byte
- 每個(gè)用戶分別使用多少 Byte
- 總共多少成功的 GET
- 訪問量前 10 位主機(jī)名
- 訪問量前 10 位 MIME
-b 為賬單模式,輸出
- 國(guó)際 URL 的訪問量 (主機(jī)名不是以 .au 結(jié)尾)
- 本地 URL 的訪問量
- 總花費(fèi) ($) 單價(jià)為:國(guó)際 $1024/MB,本地 $4096/MB
-u -s -b 為三選一
-t 為輸出 CPU 時(shí)間,輸出
- 平均處理每行使用時(shí)間
-t 可有可無。
寫代碼
首先,先處理一行。也就是:
- data = '1183245989.174 0.129 137.157.56.174 200 1277 GET http://linux.pacific.net.au/linux/packman/suse/10.2/repodata/repomd.xml "text/xml"'
復(fù)制代碼
要把這些資料提取出來,用正則(因?yàn)闀簳r(shí)還不知道其它辦法,正則比較簡(jiǎn)單一些)。觀察資料后發(fā)現(xiàn)所有資料都是由空格擱開,資料之間沒有空格,所以:
- data = '1183245989.174 0.129 137.157.56.174 200 1277 GET http://linux.pacific.net.au/linux/packman/suse/10.2/repodata/repomd.xml "text/xml"'
- data = data.split(/\s/)
復(fù)制代碼
注意 Ruby 不需要分號(hào)結(jié)尾。斜線(//)和引號(hào)一樣,作為指定范圍的工具,不過斜線指定的是 RegExp,Ruby 的規(guī)則表達(dá)式類。split 是 String 類的一個(gè)方法,分割這個(gè)字符串。
現(xiàn)在,data 就是一個(gè) Array 了,裝著分割好的字符串,不用管類型,這個(gè)和 PHP 以及很多腳本語言一樣。
按前面的要求,我們需要的是:
C3 = User IP (Identification of User)
C4 = HTTP Status (200 for success)
C5 = File size sent
C6 = HTTP Request
C7 = URL
C8 = MIME quoted in doublequotes
所以:
- data = '1183245989.174 0.129 137.157.56.174 200 1277 GET http://linux.pacific.net.au/linux/packman/suse/10.2/repodata/repomd.xml "text/xml"'
- data = data.split(/\s/)
- columns = Hash.new
- columns[:user_ip] = data[2]
- columns[:http_status] = data[3]
- columns[:file_size] = data[4]
- columns[:http_request] = data[5]
- columns[:host] = data[6].match(/^http:\/\/([^\/]+)/)[1] # thanks for DennisRitchie
- columns[:mime] = data[7]
復(fù)制代碼
首先,建立了一個(gè) Hash。我暫時(shí)還不知道 columns 是不是必須先賦值 Hash.new,才能用 [],不過保險(xiǎn)起見,先這樣搞。答案是:是的。(感謝 DennisRitchie 指出)[]= 和 [] 都是 Hash (以及 Array)的方法,所以變量必須先是 Hash (Array) 才能使用這兩個(gè)方法。Hash.new 的簡(jiǎn)便方式是 {}。
然后是可能會(huì)讓 PHPer 奇怪的冒號(hào)。冒號(hào)開頭代表 Ruby 的 Symbol 類實(shí)例。何謂 Symbol 類?其實(shí)和 String 是差不多的,但是在一般語言里面即使兩個(gè) String 的內(nèi)容完全一樣,也會(huì)分配不同的內(nèi)存,這一分一放,終究非常浪費(fèi),所以 Ruby 有了 Symbol,Smalltalk 也有 Symbol,而 Java 也有 String interning,專門解決這個(gè)問題。Symbol 再多只要內(nèi)容一樣,也只會(huì)分配一次內(nèi)存。這也讓這次作業(yè)這種平行儲(chǔ)存結(jié)構(gòu)更加有效。
那么 PHP 呢?是不是每次都會(huì)分配空間呢?不是的。Zend Engine 有著復(fù)雜的系統(tǒng)來面對(duì)這個(gè)問題,當(dāng) PHP 的任意對(duì)象沒有改變的時(shí)候,內(nèi)存中只有一個(gè) copy,只有當(dāng)改變的時(shí)候才會(huì)分配空間,而這一切對(duì)于用戶都是透明的。
另外,作業(yè)要求從 URL 中提取主機(jī)名。這在 PHP 中是很簡(jiǎn)單的,當(dāng)然也是 Perl 的強(qiáng)項(xiàng),而 Ruby 只是把它很優(yōu)雅地 OO 化了而已。
先前說過,斜線字符串代表規(guī)則表達(dá)式,而 RegExp 有一個(gè)方法叫做 match,其實(shí)和 PHP 的 preg_match 是一樣的。Ruby 也有 =~ 操作符,但是由于本人對(duì) Perl 實(shí)在不熟,所以沒覺得好用,就用 match 吧。match 方法把 data[6] 和 RegExp 對(duì)比,然后返回捕獲的結(jié)果。由于 Ruby 的對(duì)象機(jī)制很靈活,所以,直接在這個(gè) function call 的后面加上 [1] 也可以([0] 是整個(gè) match 的字符串)。
這次作業(yè)的 log 文件很簡(jiǎn)單,沒有 https/ftp 什么的,所以上面沒有寫出,當(dāng)然要寫也是很容易的。
好了,到了這一步,用 puts 來測(cè)試一下結(jié)果,成功,然后因?yàn)槭亲x入文件,每一行一個(gè)記錄,所以需要有一個(gè)東西來儲(chǔ)存記錄。首先想到的,是:
- 讀入所有 log 并儲(chǔ)存為數(shù)據(jù)結(jié)構(gòu)
- 遍歷數(shù)據(jù)結(jié)構(gòu)提取需要的數(shù)據(jù)并匯報(bào)
因?yàn)橛腥N匯報(bào)模式,都需要訪問整個(gè) log 數(shù)據(jù),所以,最好用類。
首先建立一個(gè) Log 類:
大寫開頭的名字在 Ruby 中代表常量,也用于命名類(關(guān)于這個(gè)問題 Pragmatic Ruby 里面有詳細(xì)解釋為什么)。
Ruby 不需要 {} 也不需要冒號(hào),結(jié)尾用 end 表示。(其實(shí)這一點(diǎn)我不太喜歡,因?yàn)椴皇呛芤恢拢琸eyword 直接用 end 結(jié)尾,但另外一些卻必須用 do 開始 end 結(jié)尾。)
現(xiàn)在,在 Log 實(shí)例化的時(shí)候,我希望它完成讀取 log 文件并儲(chǔ)存在自己的一個(gè)成員變量:
- class Log
- def initialize(filename, cpu_time)
- @log = Array.new
- @cpu_time = cpu_time
- File.open(filename, 'r') do |file|
- while (line = file.gets)
- data = line.split(/\s/)
- columns = Hash.new
- columns[:user_ip] = data[2]
- columns[:http_status] = data[3]
- columns[:file_size] = data[4]
- columns[:http_query] = data[5]
- columns[:host] = /^http:\/\/([^\/]+)/.match(data[6])[1]
- columns[:mime] = data[7]
- @log << columns
- end
- end
- end
- end
復(fù)制代碼
注:因?yàn)檫@次作業(yè)交晚了,所以比較趕工,程序也比較亂,這不是 Ruby 的作風(fēng),也不是我的作風(fēng)……
def 是 Ruby 中定義方法的 keyword。initialize 是類實(shí)例化的時(shí)候會(huì)使用的函數(shù),大概類似于 constructor。這個(gè)函數(shù)使用兩個(gè)參數(shù),一個(gè)是文件名,一個(gè)是,是否記錄 CPU 時(shí)間。
@log 和 @cpu_time 是這個(gè)類的實(shí)例變量(instance variable),前面加一個(gè) @ 以辯別。加 @@ 則是類變量(class variable)。
首先賦值,@log 用來記錄日志條目,所以用 Array.new,這也是我現(xiàn)在知道的為數(shù)不多的 Ruby 數(shù)據(jù)結(jié)構(gòu)之一……
@cpu_time 直接復(fù)制參數(shù)即可。
下面,Ruby magic 來了。下面的部分基本上屬于 Ruby specific 的了。
File 是 Ruby 的文件類,用于簡(jiǎn)單文件操作。要操作文件,必須先打開文件。查了 Ruby Ref 之后,發(fā)現(xiàn) File 有 new 和 open 兩個(gè)方法用于打開文件,有何不同?
參數(shù)都是一樣的,不同在于,new 直接打開文件,完了。
open 可以像 new 一樣用,也可以:附加一個(gè) block。
如果 open 附加一個(gè) block,那么在 block 執(zhí)行完畢之后,文件將會(huì)自動(dòng)關(guān)閉,非常方便。
剛剛開始看到這里的時(shí)候,也許比較混亂吧,我剛接觸這個(gè)東西的時(shí)候也一樣。下面就詳細(xì)解釋一下工作原理吧。
用 pseudo-code 寫一個(gè)文件類,只有三個(gè)方法:
- class File
- {
- method open(filename, mode)
- {
- // open file...
- return handle
- }
- method close()
- {
- // close file handle...
- }
- }
復(fù)制代碼
好,現(xiàn)在我們要用這個(gè)類了,但是每次 open,我們都必須記得要對(duì)應(yīng) close。有沒有辦法讓這個(gè)過程自動(dòng)化?有的。加入下面一個(gè)方法:
- class File
- {
- // method open()...
- // method close()...
- method open_and_proc(filename, mode, &block)
- {
- file_handle = open_file(filename) # blah blah
- yield file_handle
- self.close
- }
- }
復(fù)制代碼
這里,yield 和 return 一樣,都將離開正在執(zhí)行的代碼塊(method open_and_proc)。但是不一樣的是,yield 是暫時(shí)離開去執(zhí)行第三個(gè)參數(shù) block(也是一個(gè)代碼塊),然后再回來繼續(xù)執(zhí)行 open_and_proc。不太好理解?再換一個(gè)例子吧。
在 Ruby 中,有一個(gè)很 handy 的整數(shù)方法 times。比如說,想輸出 10 句 "Hello World!",那么可以這樣:
- 10.times do
- puts "Hello World!"
- end
復(fù)制代碼
這段程序?qū)⑤敵?10 次 Hello, World!
這樣也許不太清楚,下面這個(gè)版本用 {} 代替 do...end,應(yīng)該更清楚一些。
- 10.times {
- puts "Hello World!"
- }
復(fù)制代碼
看看 Integer 中 times 的定義吧。如果用 Ruby 表示,times 的定義大概是這樣:
- class Integer
- def times(&block)
- i = 0
- while (i < self - 1)
- yield i
- end
- end
- end
復(fù)制代碼
block 是代碼塊,而 yield 執(zhí)行這個(gè)代碼塊,而且傳入一個(gè)參數(shù)。代碼塊如何得到這個(gè)參數(shù)?用 ||。這個(gè)記號(hào) Smalltalker 一定很熟悉。所以這個(gè)代碼塊還可以這樣用:
10.times { |i|
puts "#{i} Hello World!"
}
[/code]
注意上面的 |i|,這樣就接受了這個(gè)參數(shù)。上面的代碼將輸出:
0 Hello World!
1 Hello World!
2 Hello World!
3 Hello World!
4 Hello World!
5 Hello World!
6 Hello World!
7 Hello World!
8 Hello World!
9 Hello World!
也許一開始這種作法并不討好,感覺還有些混亂。當(dāng)你實(shí)際用到之后,就會(huì)慢慢覺得很方便了。
在 Ruby 中,iterator 的實(shí)現(xiàn)也是用這個(gè)辦法。不管是 Hash 還是 Array,都有 each 方法,這個(gè)方法即是用來遍歷數(shù)據(jù)結(jié)構(gòu)。例如:
- arr = ["foo", "bar", "egg", "span"]
復(fù)制代碼
這是一個(gè)數(shù)組,如果在 PHP/C... 中,要輸出這個(gè)數(shù)組所有項(xiàng)目,可以用:
- foreach (arr as a) {
- print a
- }
- // 或者...
- for (i = 0, max = count(arr); i < max; ++i)
- print a
復(fù)制代碼
第二種方法需要你了解這個(gè)數(shù)據(jù)結(jié)構(gòu)的內(nèi)部結(jié)構(gòu),不可取。第一種方法在 Ruby 中則是用 block 實(shí)現(xiàn)的:
- # Ruby code
- arr.each {
- |item|
- puts item
- }
復(fù)制代碼
現(xiàn)在回到 File 類。File 類提供了一個(gè) open 方法,這個(gè)方法可以接受一個(gè) block。如果傳入 block,則 File 將打開文件,傳入 block,執(zhí)行 block,執(zhí)行完后關(guān)閉文件,非常好用。
- while (line = file.gets)
- end
復(fù)制代碼
這一段則是 Ruby 的簡(jiǎn)單文件讀取,gets 將讀取文件中的一行,由于有自帶計(jì)數(shù)器,只需要上面一句即可遍歷整個(gè)文件。
后面只需要抄最先的代碼,即可把每行的數(shù)據(jù)進(jìn)行分析并輸入一個(gè) Hash。把這個(gè) Hash 加入 @log 數(shù)組,用 << 操作符,我最先猜 append 方法,結(jié)果沒有,看來偶爾也會(huì)失誤(我猜 split,match,規(guī)則表達(dá)式語法都猜對(duì)了,open 也基本上猜對(duì)了)。
然后需要根據(jù)要求加入幾個(gè)方法 parse/summary/bill,分別對(duì)應(yīng)幾個(gè)需求。然后再加入主驅(qū)動(dòng)程序,這個(gè)程序就算完了。全程序:
- #!/usr/local/bin/ruby
- # class definition
- class Log
- def initialize(filename, cpu_time)
- @log = Array.new
- @cpu_time = cpu_time
- File.open(filename, 'r') do |file|
- while (line = file.gets)
- data = line.split(/\s/)
- columns = Hash.new
- columns[:user_ip] = data[2]
- columns[:http_status] = data[3]
- columns[:file_size] = data[4]
- columns[:http_query] = data[5]
- columns[:host] = /^http:\/\/([^\/]+)/.match(data[6])[1]
- columns[:mime] = data[7]
- @log << columns
- end
- end
- end
-
- def parse(user_ip)
- all_line = 0
- total_line = 0
- total_get = 0
- total_200 = 0
- total_byte = 0
- host_count = Hash.new
- mime_count = Hash.new
-
- @log.each do |a_log|
- all_line += 1
-
- next if (a_log[:user_ip] != user_ip)
-
- total_line += 1
- total_get += 1 if (a_log[:http_query] == 'GET')
- total_200 += 1 if (a_log[:http_status] == '200')
- total_byte += a_log[:file_size].to_i
- if (host_count[a_log[:host]])
- host_count[a_log[:host]] += 1
- else
- host_count[a_log[:host]] = 1
- end
- if (mime_count[a_log[:mime]])
- mime_count[a_log[:mime]] += 1
- else
- mime_count[a_log[:mime]] = 1
- end
- end
- avg_byte = total_byte / total_200
-
- max = 0
- max_key = ''
- host_count.each do |key, val|
- if (val > max)
- max = val
- max_key = key
- end
- end
- fav_host = max_key
- max = 0
- max_key = ''
- mime_count.each do |key, val|
- if val > max
- max = val
- max_key = key
- end
- end
-
- fav_mime = max_key
-
- puts "Total number of line: #{total_line}"
- puts "Total GET: #{total_get}"
- puts "Total HTTP 200: #{total_200}"
- puts "Total bytes: #{total_byte}"
- puts "Most visited host: #{fav_host}"
- puts "Most visited mime: #{fav_mime}"
-
- if (@cpu_time)
- t = Process.times
- puts "CPU Time per line: #{t.utime / all_line}"
- end
- end
-
- def summary
- total_line = 0
- total_byte = 0
- user_byte = Hash.new
- total_get = 0
- top_10_host = Hash.new
- top_10_mime = Hash.new
-
- @log.each do |a_log|
- total_line += 1
- total_byte += a_log[:file_size].to_i
-
- if (user_byte[a_log[:user_ip]])
- user_byte[a_log[:user_ip]] += a_log[:file_size].to_i
- else
- user_byte[a_log[:user_ip]] = a_log[:file_size].to_i
- end
-
- total_get += 1 if a_log[:http_status] == '200' # only succeed GETs
-
- if (top_10_host[a_log[:host]])
- top_10_host[a_log[:host]] += 1
- else
- top_10_host[a_log[:host]] = 1
- end
-
- if (top_10_mime[a_log[:mime]])
- top_10_mime[a_log[:mime]] += 1
- else
- top_10_mime[a_log[:mime]] = 1
- end
- end
-
- top_10_host = top_10_host.sort do |a, b|
- b[1] <=> a[1]
- end
-
- top_10_mime = top_10_mime.sort do |a, b|
- b[1] <=> a[1]
- end
-
- puts "Total line read: #{total_line}"
- puts "Total bytes consumed: #{total_byte}"
- puts "Byte consumed grouped by user:"
- user_byte.each do |user, byte|
- puts "#{user} used #{byte} bytes."
- end
- puts "Total successful GET: #{total_get}"
-
- puts "Top 10 hosts:"
- i = 1
- top_10_host.to_a.each do |a|
- puts "No.#{i} #{a[0]} hits: #{a[1]}"
- i += 1
- break if i > 10
- end
-
- puts "Top 10 mime:"
- i = 1
- top_10_mime.to_a.each do |a|
- puts "No.#{i} #{a[0]} hits: #{a[1]}"
- i += 1
- break if i > 10
- end
- if (@cpu_time)
- t = Process.times
- puts "CPU Time per line: #{t.utime / total_line}"
- end
- end
-
- def bill
- total_line = 0
- user_bill = Hash.new
- world_url = 0
- world_byte = 0
- domes_url = 0
- domes_byte = 0
-
- @log.each do |a_log|
- total_line += 1
- if (user_bill[a_log[:user_ip]] == nil)
- user_bill[a_log[:user_ip]] = Hash.new
- user_bill[a_log[:user_ip]][:world_url] = 0
- user_bill[a_log[:user_ip]][:world_byte] = 0
- user_bill[a_log[:user_ip]][:domes_url] = 0
- user_bill[a_log[:user_ip]][:domes_byte] = 0
- end
- if (a_log[:host].match(/\.au$/))
- user_bill[a_log[:user_ip]][:domes_url] += 1
- user_bill[a_log[:user_ip]][:domes_byte] += a_log[:file_size].to_i
- else
- user_bill[a_log[:user_ip]][:world_url] += 1
- user_bill[a_log[:user_ip]][:world_byte] += a_log[:file_size].to_i
- end
- end
- user_bill.each do |user, bill|
- total = 0
- puts "User: #{user}"
- puts "International visits: #{bill[:world_byte]} bytes in #{bill[:world_url]} urls"
- total += bill[:world_byte].to_f / 1024
- puts "Domestic visits: #{bill[:domes_byte]} bytes in #{bill[:domes_url]} urls"
- total += bill[:domes_byte].to_f / 1024 * 4
- puts "Total cost: $#{total}"
- puts '============================================'
- end
-
- if (@cpu_time)
- t = Process.times
- puts "CPU Time per line: #{t.utime / total_line}"
- end
- end
- end
- # end class definition
- if (ARGV.size < 1)
- puts 'Usage: logwhacker [-u ip_address] [-s] [-t] log_file'
- exit
- end
- if (ARGV[0] == '-u')
- if (ARGV[1].match(/[1-9][0-9]{0,2}\.[1-9][0-9]{0,2}\.[1-9][0-9]{0,2}\.[1-9][0-9]{0,2}/))
- mode = 'USER'
- user_ip = ARGV[1]
- ARGV.shift # removes '-u'
- ARGV.shift # removes ip
- else
- puts 'Error: wrong arguments for -u.'
- exit
- end
- elsif (ARGV[0] == '-s')
- mode = 'SUM'
- ARGV.shift # removes '-s'
- elsif (ARGV[0] == '-b')
- mode = 'BILL'
- ARGV.shift # removes '-b'
- else
- puts 'Error: arguments required [-u] or [-s] or [-b]'
- exit
- end
- if (ARGV[0] == '-t')
- ARGV.shift # removes '-t'
- cpu_time = true
- end
- if (ARGV[0] == nil)
- puts 'Error: log file name required.'
- exit
- else
- filename = ARGV.shift
- end
- logwhacker = Log.new(filename, cpu_time)
- if (mode == 'USER')
- logwhacker.parse(user_ip)
- elsif (mode == 'SUM')
- logwhacker.summary
- elsif (mode == 'BILL')
- logwhacker.bill
- end
復(fù)制代碼
[ 本帖最后由 dz902 于 2007-8-23 02:40 編輯 ] |
|