みどりねこ日記

よくわからないけど、頑張りますよ。

Parsec を使って Apache のログファイルをパースしてみる2

さて、ログの行をパースすることができるようになったので、これをコマンドラインツールにしましょう。今回は IO と do 記法を用いて作業します。ですので、少なくとも基本は知っていることが望ましいです。もし不安なら、Learn You a Haskell を読みましょう。

ユースケースを見ると、圧縮されていないログファイル一つがあるとして、そのログファイルは複数行のログを保持していると推測できます。私たちのパーサは一行をパースします。なので、どうすればいいかを書いてみましょう。

  1. ファイルを読み込む
  2. 行ごとに切り離す
  3. 各行をパースする
  4. 結果を表示する

最初の操作は Prelude にある readFile を使います。つまり、標準でインポートされるということです。 readFile は IO String 型を持ちます。これは、 IO の文脈を壊せないことを表しています。もしこのことで混乱したならば、上のリンクのチャプターを読んでみてください。

main = do
	file <- readFile "logfile.txt"

main 関数、またはアクションは、すべての Haskell プログラムのスタート地点です。 readFile "logfile.txt" の結果を file という名に束縛するところから始めます。今、 file の型は String です。 String 型ということは、 Prelude の関数 lines に渡せるということですね!しかし、 readFile は IO モナドのなかに包んで渡してくるので、そのままでは lines に渡せません。ですので、 <- ではなく let による束縛を使いましょう。

main = do
	file <- readFile "logfile.txt"
	let logLines = lines file

良い感じですね!もしこれをコンパイルしようとすると、エラーを吐かれます。なぜなら、 do 記法の最後は let による束縛で終わらせることはできないからです。直しましょう。しかしその前に、 Parsec の parse を見てみましょう。ここでは parse の型を書きませんが、 ghci で

 :t parse

とすると見ることができます。さっぱりわかりませんね!
しかし幸運にも、この関数 parse は使いやすいです。基本的には以下のように使います:

parse line "(test)" testLine

つまり、なにか一つ Parser をとり、パーサ名 String をとり、パース対象の String を受け取る関数です。このコードは Either ParserError String を返します。しかし、私たちのパース対象は [String] なので(lines による分割)、 mapM (map のモナド版)を用いてログファイル全体をパースしましょう。

main = do
	file <- readFile "logfile.txt"
	let logLines = lines file
	result <- mapM (parse line "(test)") logLines

result の方は [Either ParseError String] です。これを展開するために、 Prelude の either を用いましょう。 either は2つ関数を取り、 Left の場合は最初の関数を適用、 Right の場合は2つ目の関数を適用する関数です。取り扱うものが Either のリストなので、 either もマッピングする必要があります。 ParseError は Show クラスのインスタンスなので、どちらのケースでも単純に関数 print を使って印字することにしましょう。

main = do
	file <- readFile "logfile.txt"
	let logLines = lines file
	result <- mapM (parse line "(test)") logLines
	mapM_ (either print print) result

できました!このスクリプトはパーサの各結果、エラーまたはデータ型 LogLine を印字します。

あまり努力してもいないのにもかかわらず、他言語ではなかなか難しいことを、このスクリプトは少ないメモリで実現します!遅延評価のマジックの恩恵により、各行は読まれ、パースされ、印字され、ガベージコレクションされます。とは言っても、これはあまり効率のいい実装ではないので、もっと最適化してみましょう。