Scala で書き捨てスクリプト (3)
Scala は良いと思う。でも最近書いてなかったので、アクセスログのリファラを数えるという (変数に型のないようなスクリプト言語でさくっとすます類いの) 作業をやってみた。
% cat access.log ... 127.0.0.1 blog.8-p.info - [19/Jul/2009:06:23:26 +0000] "GET /2009/wp-content/themes/b8i/ALL.js HTTP/1.1" 200 90381 "http://blog.8-p.info/2009/03/textfield-js" "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; ja-jp) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19" 127.0.0.1 blog.8-p.info - [19/Jul/2009:06:23:27 +0000] "GET /favicon.ico HTTP/1.1" 200 894 "http://blog.8-p.info/2009/03/textfield-js" "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; ja-jp) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19" ... % scala a.scala access.log ... (7,http://www.tumblr.com/dashboard) (8,http://twitturls.com/) (8,http://www.reddit.com/r/ja/) ... %
満足した。
最初に動いたコード
import java.io._
import scala.util.matching.Regex
class File(path: String) extends Iterator[String] {
val reader = new BufferedReader(new FileReader(path))
var line: String = reader.readLine
def hasNext(): Boolean = {
if (line == null) {
line = reader.readLine
}
return line != null
}
def next(): String = {
val result = line
line = null
return result
}
}
val file = new File(args(0))
val pattern = new Regex("(.+?) (.+?) - \\[(.+?)\\] \"(.+?)\" (\\d+) (\\d+) \"(.+?)\" \"(.+?)\"")
var countOf: Map[String, Int] = Map()
for (ln <- file) {
pattern.findFirstMatchIn(ln) match {
case Some(m) => {
val from = m.group(7)
if (m.group(2) == "blog.8-p.info" && from != "-" &&
from.indexOf("http://blog.8-p.info/") != 0) {
countOf += from -> (countOf.getOrElse(from, 0) + 1)
}
}
case None => ;
}
}
print(countOf.map((pair) => {
pair._2 -> pair._1
}).toList.sort((a, b) => {
a < b
}).mkString("\n") + "\n")
個々の処理は必要十分な記述量だと思うけど File クラスはないなあ…
今回の敗因はログに UTF-8 として不正な文字列 (C 言語っぽく書くと “GET /uldq\xc9\xcc\xd2\xb5\xb0\xe6\xb1\xbe\x2e.rar HTTP/1.1″ というものだった) があったことで、この行で Scala.io.Source#getLines が java.nio.BufferUnderflowException を吐いてしまうため直接 java.io. をさわることになった。
しかし Scala では代入が値を返さないので
while ((ln = reader.readLine) != null) {
と書けず、break もないので
while (true) {
ln = reader.readLine;
if (ln == null) {
break;
}
ともいかず、とはいえフラグ変数は嫌だったのでクラスにまとめたという経緯がある。
あと、正規表現はリテラルがほしい。\ は文字列でも正規表現でもメタキャラクタなので、正規表現のパーサに \ を渡すために \\ と書くのは Emacs-Lisp みたいで手間だった。
短くした
書き捨てなので動いたところで終わるのだけど、すこし調べたら短くできた。
import java.io._
import scala.util.matching.Regex
val reader = new BufferedReader(new FileReader(args(0)))
val pattern = new Regex("""(.+?) (.+?) - \[(.+?)\] "(.+?)" (\d+) (\d+) "(.+?)" "(.+?)""" + "\"") // "
var countOf: Map[String, Int] = Map()
var ln = ""
while ({ ln = reader.readLine; ln != null }) {
pattern.findFirstMatchIn(ln) match {
case Some(m) => {
val from = m.group(7)
if (m.group(2) == "blog.8-p.info" && from != "-" &&
from.indexOf("http://blog.8-p.info/") != 0) {
countOf += from -> (countOf.getOrElse(from, 0) + 1)
}
}
case None => ;
}
}
print(countOf.map((pair) => {
pair._2 -> pair._1
}).toList.sort((a, b) => {
a < b
}).mkString("\n") + "\n")
while で代入文の返り値を使えないのは Scalaでファイル操作 を参考に書きなおした。ln のスコープがちょっといやかなあ。でも File クラスがいないのは見た目いい。
正規表現は """ の文字列リテラルをつかってエスケープを減らしてみた。""" でくくられた文字列は " で終われない のと、Emacs の色づけを直すための // ” がださい。
” が連続しなければいい
val pattern = new Regex("""^(.+?) (.+?) - \[(.+?)\] "(.+?)" (\d+) (\d+) "(.+?)" "(.+?)"$""")
先頭と末尾に明示的にマッチさせて、見た目の不自然さを減らしてみた。
他言語との比較: Ruby
コメントをもらいました。
ためしに perl や ruby も書いて比べてみるの希望。
まず Ruby で書いてみました。
count_of = Hash.new do
0
end
PATTERN = /^(.+?) (.+?) - \[(.+?)\] "(.+?)" (\d+) (\d+) "(.+?)" "(.+?)"$/
File.open(ARGV[0]) do |f|
f.each do |ln|
if ln =~ PATTERN
from = $7
if $2 == 'blog.8-p.info' and from != '-' and
from !~ %r{^http://blog.8-p.info/}
count_of[from] += 1
end
end
end
end
puts(count_of.map do |k, v|
[v, k]
end.sort.map do |ary|
ary.join("\t")
end.join("\n"))
基本的には Scala のべた移植で、変更している部分は
- Ruby で「文字列の先頭にある部分文字列がある?」というのをみるには String#index より正規表現を使いがち
- Array#join は再帰的に配列をなめてしまうので map に、あと明示的に Object#inspect よりはタブ区切りにするだろう
という2つです。見た目はすっきりしたなあ。ただし速度は遅いです。
% ls -lh access.log -rw-r--r-- 1 kzys staff 169M 10 1 21:27 access.log % ruby -v ruby 1.8.7 (2008-08-11 patchlevel 72) [universal-darwin10.0] % time ruby a.rb access.log ... ruby a.rb access.log 28.61s user 0.63s system 97% cpu 30.128 total % time scala a.scala access.log ... scala a.scala access.log 14.19s user 0.65s system 83% cpu 17.742 total %
time(1) で測るのは今回のユースケースではいいけど、たとえば Web アプリケーションならプロセスの起動時間なんて無視して単位時間あたりにさばけるリクエスト数で速い/遅いをいうべきだと思います。
他言語との比較: Perl
つぎに Perl です。
use strict;
use warnings;
my %count_of;
my $PATTERN = qr/^(.+?) (.+?) - \[(.+?)\] "(.+?)" (\d+) (\d+) "(.+?)" "(.+?)"$/;
open(my $file, '<', $ARGV[0]);
while (my $ln = <$file>) {
if ($ln =~ $PATTERN) {
my $from = $7;
if ($2 eq 'blog.8-p.info' and $from ne '-' and
$from !~ qr{^http://blog.8-p.info/}) {
$count_of{$from} += 1
}
}
}
close($file);
print((join "\n", map {
join "\t", @$_;
} sort {
$a->[0] <=> $b->[0];
} map {
[ $count_of{$_} => $_ ];
} keys %count_of), "\n");
基本的には Ruby に my とか ; とかつけた感じです。配列 (arrayref) に対して大小関係をとれなかったので、なかを直接みてみます。しかしこれが速くて。
% perl -v This is perl, v5.10.0 built for darwin-thread-multi-2level (with 2 registered patches, see perl -V for more detail) Copyright 1987-2007, Larry Wall Perl may be copied only under the terms of either the Artistic License or the GNU General Public License, which may be found in the Perl 5 source kit. Complete documentation for Perl, including FAQ lists, should be found on this system using "man perl" or "perldoc perl". If you have access to the Internet, point your browser at http://www.perl.org/, the Perl Home Page. % time perl a.pl access.log ... perl a.pl access.log 5.88s user 0.33s system 95% cpu 6.519 total %
すごいなあ。どこで差がついたかはまた後で調べたいです。
ためしに perl や ruby も書いて比べてみるの希望。
ちょっと追記した。Perl 速い…
おーありがとうございます。コンパイル時間いれても scala の方がはやいとは。しかも perl が…
計らなければわからんもんですねえ。