Parser-Combinator

| # Comments | 1 Trackback
Scalaは、構文解析のためのライブラリ( Parser-Combinator )を標準で持っています。
そのParser-Combinatorを試してみました。

課題としては、Apacheのアクセスログ1行を解析して、ログクラスのインスタンスを生成するものです。

環境は以下の通りです。

  • Scala-2.8.0.RC3

ベースクラス AccessLogParser

Apacheのアクセスログには、一般にCommonLogフォーマットと、CombinedLogフォーマットがありますので、共通で利用するParserを定義する親クラスを作成します。

import _root_.java.util.{ Date, Locale }
import _root_.java.text.SimpleDateFormat

import _root_.scala.util.parsing.combinator.RegexParsers

trait AccessLogParser extends RegexParsers {
  override val whiteSpace = "".r

  lazy val remoteHost: Parser[String] = "[^ ]+".r
  lazy val remoteUser: Parser[Option[String]] = "[^ ]+".r ^^ {
    case "-" => None
    case x   => Some(x)
  }
  lazy val requestedTime: Parser[Date] = elem('[') ~> "[^]]+".r <~ elem(']') ^^ {
    new SimpleDateFormat("dd/MMM/yyyy:HH:mm:ss Z", Locale.US).parse(_)
  }

  lazy val method:      Parser[String] = "[A-Z]+".r
  lazy val requestPath: Parser[String] = "[^ ]+".r
  lazy val protocol:    Parser[String] = "[^\"]+".r

  lazy val statusCode: Parser[Int] = "[0-9]+".r ^^ { Integer.parseInt(_) }

  lazy val contentLength: Parser[Option[Int]] = "[0-9]+|-".r ^^ {
    i =>
      try { Some(Integer.parseInt(i)) } catch { case e => None }
  }
  lazy val referer: Parser[Option[String]] =
    "\"" ~> "[^\"]*".r <~ "\"" ^^ {
      r =>
        if(r != "" && r != "-") { Some(r) } else { None }
    }
  lazy val userAgent: Parser[Option[String]] =
    "\"" ~> "(?:\\\\\"|[^\"])*".r <~ "\"" ^^ {
      ua =>
        if(ua != "") { Some(ua.replaceAll("\\\\\"", "\"")) } else { None }
    }
}

アクセスログに含まれる各要素を抽出するためのParserの定義になります。

CommonLogParser

CommonLogフォーマットのログをパースするクラスです。

import _root_.java.io.{ BufferedReader, InputStreamReader }
import _root_.java.util.Date

import _root_.scala.util.parsing.input.CharSequenceReader

case class CommonLog (
  remoteHost: String,
  remoteUser: Option[String],
  requestedTime: Date,
  method: String,
  requestPath: String,
  protocol: String,
  statusCode: Int,
  contentLength: Option[Int]
)

object CommonLogParser extends AccessLogParser {

  lazy val commonLog: Parser[CommonLog] = 
    remoteHost ~ " - " ~ remoteUser ~ " " ~ requestedTime ~ " \"" ~ method ~ " " ~ requestPath ~ " " ~ protocol ~ "\" " ~ statusCode ~ " " ~ contentLength ^^ {
      case remoteHost ~ " - " ~ remoteUser ~ " " ~ requestedTime ~ " \"" ~ method ~ " " ~ requestPath ~ " " ~ protocol ~ "\" " ~ statusCode ~ " " ~ contentLength  =>
        CommonLog(remoteHost, remoteUser, requestedTime, method, requestPath, protocol, statusCode, contentLength)
    }

  def main(args: Array[String]) {
    val reader = new BufferedReader(new InputStreamReader(System.in, "utf8"))
    for(line <- Stream.continually(reader.readLine).takeWhile(_ != null)) {
        println(commonLog(new CharSequenceReader(line)).get)
    }
  }
}

AccessLogParserクラスで定義したそれぞれの要素を ~メソッドでつないで、1行のログをパースするよう実装します。

CommonLogフォーマットなので、リファラとユーザーエージェントは含みません。

使い方

標準入力からログを流し込みます。

$ cat common.log | scala-2.8 CommonLogParser

CommonLogクラスのインスタンスの文字列表現が標準出力に出力されます。

CombinedLogParser

CombinedLogフォーマットをパースするクラスです。

import _root_.java.io.{ BufferedReader, InputStreamReader }
import _root_.java.util.Date

import _root_.scala.util.parsing.input.CharSequenceReader

case class CombinedLog (
  remoteHost: String,
  remoteUser: Option[String],
  requestedTime: Date,
  method: String,
  requestPath: String,
  protocol: String,
  statusCode: Int,
  contentLength: Option[Int],
  referer: Option[String],
  userAgent: Option[String]
)

object CombinedLogParser extends AccessLogParser {

  lazy val combinedLog: Parser[CombinedLog] = 
    remoteHost ~ " - " ~ remoteUser ~ " " ~ requestedTime ~ " \"" ~ method ~ " " ~ requestPath ~ " " ~ protocol ~ "\" " ~ statusCode ~ " " ~ contentLength ~ " " ~ referer ~ " " ~ userAgent ^^ {
      case remoteHost ~ " - " ~ remoteUser ~ " " ~ requestedTime ~ " \"" ~ method ~ " " ~ requestPath ~ " " ~ protocol ~ "\" " ~ statusCode ~ " " ~ contentLength ~ " " ~ referer ~ " " ~ userAgent =>
        CombinedLog(remoteHost, remoteUser, requestedTime, method, requestPath, protocol, statusCode, contentLength, referer, userAgent)
    }

  def main(args: Array[String]) {
    val reader = new BufferedReader(new InputStreamReader(System.in, "utf8"))
    for(line <- Stream.continually(reader.readLine).takeWhile(_ != null)) {
        println(combinedLog(new CharSequenceReader(line)).get)
    }
  }
}

こちらは、リファラとユーザーエージェントのParserが追加されています。

使い方

同様に、標準入力からCombinedLogを流し込みます。

$ cat combined.log | scala-2.8 CombinedLogParser

CombinedLogクラスのインスタンスの文字列表現が標準出力に出力されます。

トラックバック(1)

HBaseを使ったMapReduceを実装してみました。題材はいつもと同じですが... 続きを読む

comments powered by Disqus

Twitter Icon

AdSense

Creative Commons License
このブログはクリエイティブ・コモンズでライセンスされています。
Powered by Movable Type 5.14-ja

Google検索

カスタム検索

2013年10月

    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31