- 论坛徽章:
- 0
|
谨写此篇希望为 Ruby 和 CU Ruby 版的发展尽一份薄力。因为 Ruby 真的是很优雅、很强大而又很灵活的语言。
前言
开始知道 Ruby 是什么时候已经忘了,但是确实一直深深被其所吸引。用 PHP 差不多 3 年了,却一直不喜欢它过于随意的方式,最不喜欢的是把所有函数都放在全局空间,名字和函数位置也很让人诟病。但是一直没有一个特别的需求一定要学 Ruby,所以也就随意看了些 Ruby 的教程,后来看到 RoR,很受启发,用 PHP 5 的新功能写了一个类似的小框架。但还是没有真正学 Ruby,只觉得,哦,方便,哦,清楚,哦,不错,就完了。
终于,这一天到来了。这次学习 System Adminstration,第一个作业,写一个分析访问记录(log)的小程序,要求,用脚本(当然你想用 C++ 没人拦你)。而作业要求明明写到,Python/Perl/Ruby。Python 最近也在看,因为 Ruby 太常拿来给 Python 比较了。但是就 Python 来说,OO 得不够彻底,虽然在很多情况下更为清晰(比如 list comprehension),但是对于我这种完美主义者来说,语言的 consistency 比较重要,所以没有更深地接触 Python。Perl 就不说了,就我个人来说不愿意花太多时间去记一门语言的语法和关键词等等,因为已经要花很多时间来记 Unix 和 Emacs 的命令什么的了。只想轻松一点。
后来去问讲师,可以用 PHP 么?我其实觉得应该是可以的,毕竟都是 Scripting Language,某种意义上。但是他明确回答不行。自然最后就只有 Ruby 了。
开始
我先说明:我还算比较了解 PHP 关于中小型网站开发的部分,PHP 5 也有接触。另外 C++/Java 都会一些;关于 Ruby 的资料看得不多也不系统,很多时候就是蜻蜓点水类地泛泛而看。
开发环境
这是一个很小的程序,所以不太需要太强大的 IDE,EditPlus 足够了,但是我想要更融合 Ruby 的编辑器,所以搜索了之后选定 RDE,很简单,也很小。下载 RDE 之后,你需要选择 Ruby 的位置,然后即可进入。
作业
这次的作业也很简单,要求是:
- 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"
- ...
复制代码
分析类似于上面这种 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
- 最常去的域名/主机名
- 最常下载的 MIME
-s 为报告模式,输出
- 总共多少行记录
- 总共传送多少 Byte
- 每个用户分别使用多少 Byte
- 总共多少成功的 GET
- 访问量前 10 位主机名
- 访问量前 10 位 MIME
-b 为账单模式,输出
- 国际 URL 的访问量 (主机名不是以 .au 结尾)
- 本地 URL 的访问量
- 总花费 ($) 单价为:国际 $1024/MB,本地 $4096/MB
-u -s -b 为三选一
-t 为输出 CPU 时间,输出
- 平均处理每行使用时间
-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"'
复制代码
要把这些资料提取出来,用正则(因为暂时还不知道其它办法,正则比较简单一些)。观察资料后发现所有资料都是由空格搁开,资料之间没有空格,所以:
- 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/)
复制代码
注意 Ruby 不需要分号结尾。斜线(//)和引号一样,作为指定范围的工具,不过斜线指定的是 RegExp,Ruby 的规则表达式类。split 是 String 类的一个方法,分割这个字符串。
现在,data 就是一个 Array 了,装着分割好的字符串,不用管类型,这个和 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]
复制代码
首先,建立了一个 Hash。我暂时还不知道 columns 是不是必须先赋值 Hash.new,才能用 [],不过保险起见,先这样搞。答案是:是的。(感谢 DennisRitchie 指出)[]= 和 [] 都是 Hash (以及 Array)的方法,所以变量必须先是 Hash (Array) 才能使用这两个方法。Hash.new 的简便方式是 {}。
然后是可能会让 PHPer 奇怪的冒号。冒号开头代表 Ruby 的 Symbol 类实例。何谓 Symbol 类?其实和 String 是差不多的,但是在一般语言里面即使两个 String 的内容完全一样,也会分配不同的内存,这一分一放,终究非常浪费,所以 Ruby 有了 Symbol,Smalltalk 也有 Symbol,而 Java 也有 String interning,专门解决这个问题。Symbol 再多只要内容一样,也只会分配一次内存。这也让这次作业这种平行储存结构更加有效。
那么 PHP 呢?是不是每次都会分配空间呢?不是的。Zend Engine 有着复杂的系统来面对这个问题,当 PHP 的任意对象没有改变的时候,内存中只有一个 copy,只有当改变的时候才会分配空间,而这一切对于用户都是透明的。
另外,作业要求从 URL 中提取主机名。这在 PHP 中是很简单的,当然也是 Perl 的强项,而 Ruby 只是把它很优雅地 OO 化了而已。
先前说过,斜线字符串代表规则表达式,而 RegExp 有一个方法叫做 match,其实和 PHP 的 preg_match 是一样的。Ruby 也有 =~ 操作符,但是由于本人对 Perl 实在不熟,所以没觉得好用,就用 match 吧。match 方法把 data[6] 和 RegExp 对比,然后返回捕获的结果。由于 Ruby 的对象机制很灵活,所以,直接在这个 function call 的后面加上 [1] 也可以([0] 是整个 match 的字符串)。
这次作业的 log 文件很简单,没有 https/ftp 什么的,所以上面没有写出,当然要写也是很容易的。
好了,到了这一步,用 puts 来测试一下结果,成功,然后因为是读入文件,每一行一个记录,所以需要有一个东西来储存记录。首先想到的,是:
- 读入所有 log 并储存为数据结构
- 遍历数据结构提取需要的数据并汇报
因为有三种汇报模式,都需要访问整个 log 数据,所以,最好用类。
首先建立一个 Log 类:
大写开头的名字在 Ruby 中代表常量,也用于命名类(关于这个问题 Pragmatic Ruby 里面有详细解释为什么)。
Ruby 不需要 {} 也不需要冒号,结尾用 end 表示。(其实这一点我不太喜欢,因为不是很一致,keyword 直接用 end 结尾,但另外一些却必须用 do 开始 end 结尾。)
现在,在 Log 实例化的时候,我希望它完成读取 log 文件并储存在自己的一个成员变量:
- 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
复制代码
注:因为这次作业交晚了,所以比较赶工,程序也比较乱,这不是 Ruby 的作风,也不是我的作风……
def 是 Ruby 中定义方法的 keyword。initialize 是类实例化的时候会使用的函数,大概类似于 constructor。这个函数使用两个参数,一个是文件名,一个是,是否记录 CPU 时间。
@log 和 @cpu_time 是这个类的实例变量(instance variable),前面加一个 @ 以辩别。加 @@ 则是类变量(class variable)。
首先赋值,@log 用来记录日志条目,所以用 Array.new,这也是我现在知道的为数不多的 Ruby 数据结构之一……
@cpu_time 直接复制参数即可。
下面,Ruby magic 来了。下面的部分基本上属于 Ruby specific 的了。
File 是 Ruby 的文件类,用于简单文件操作。要操作文件,必须先打开文件。查了 Ruby Ref 之后,发现 File 有 new 和 open 两个方法用于打开文件,有何不同?
参数都是一样的,不同在于,new 直接打开文件,完了。
open 可以像 new 一样用,也可以:附加一个 block。
如果 open 附加一个 block,那么在 block 执行完毕之后,文件将会自动关闭,非常方便。
刚刚开始看到这里的时候,也许比较混乱吧,我刚接触这个东西的时候也一样。下面就详细解释一下工作原理吧。
用 pseudo-code 写一个文件类,只有三个方法:
- class File
- {
- method open(filename, mode)
- {
- // open file...
- return handle
- }
- method close()
- {
- // close file handle...
- }
- }
复制代码
好,现在我们要用这个类了,但是每次 open,我们都必须记得要对应 close。有没有办法让这个过程自动化?有的。加入下面一个方法:
- 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
- }
- }
复制代码
这里,yield 和 return 一样,都将离开正在执行的代码块(method open_and_proc)。但是不一样的是,yield 是暂时离开去执行第三个参数 block(也是一个代码块),然后再回来继续执行 open_and_proc。不太好理解?再换一个例子吧。
在 Ruby 中,有一个很 handy 的整数方法 times。比如说,想输出 10 句 "Hello World!",那么可以这样:
- 10.times do
- puts "Hello World!"
- end
复制代码
这段程序将输出 10 次 Hello, World!
这样也许不太清楚,下面这个版本用 {} 代替 do...end,应该更清楚一些。
- 10.times {
- puts "Hello World!"
- }
复制代码
看看 Integer 中 times 的定义吧。如果用 Ruby 表示,times 的定义大概是这样:
- class Integer
- def times(&block)
- i = 0
- while (i < self - 1)
- yield i
- end
- end
- end
复制代码
block 是代码块,而 yield 执行这个代码块,而且传入一个参数。代码块如何得到这个参数?用 ||。这个记号 Smalltalker 一定很熟悉。所以这个代码块还可以这样用:
10.times { |i|
puts "#{i} Hello World!"
}
[/code]
注意上面的 |i|,这样就接受了这个参数。上面的代码将输出:
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!
也许一开始这种作法并不讨好,感觉还有些混乱。当你实际用到之后,就会慢慢觉得很方便了。
在 Ruby 中,iterator 的实现也是用这个办法。不管是 Hash 还是 Array,都有 each 方法,这个方法即是用来遍历数据结构。例如:
- arr = ["foo", "bar", "egg", "span"]
复制代码
这是一个数组,如果在 PHP/C... 中,要输出这个数组所有项目,可以用:
- foreach (arr as a) {
- print a
- }
- // 或者...
- for (i = 0, max = count(arr); i < max; ++i)
- print a
复制代码
第二种方法需要你了解这个数据结构的内部结构,不可取。第一种方法在 Ruby 中则是用 block 实现的:
- # Ruby code
- arr.each {
- |item|
- puts item
- }
复制代码
现在回到 File 类。File 类提供了一个 open 方法,这个方法可以接受一个 block。如果传入 block,则 File 将打开文件,传入 block,执行 block,执行完后关闭文件,非常好用。
- while (line = file.gets)
- end
复制代码
这一段则是 Ruby 的简单文件读取,gets 将读取文件中的一行,由于有自带计数器,只需要上面一句即可遍历整个文件。
后面只需要抄最先的代码,即可把每行的数据进行分析并输入一个 Hash。把这个 Hash 加入 @log 数组,用 << 操作符,我最先猜 append 方法,结果没有,看来偶尔也会失误(我猜 split,match,规则表达式语法都猜对了,open 也基本上猜对了)。
然后需要根据要求加入几个方法 parse/summary/bill,分别对应几个需求。然后再加入主驱动程序,这个程序就算完了。全程序:
- #!/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
复制代码
[ 本帖最后由 dz902 于 2007-8-23 02:40 编辑 ] |
|