Scalaは、構文解析のためのライブラリ( Parser-Combinator )を標準で持っています。
そのParser-Combinatorを試してみました。
課題としては、Apacheのアクセスログ1行を解析して、ログクラスのインスタンスを生成するものです。
環境は以下の通りです。
その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
クラスのインスタンスの文字列表現が標準出力に出力されます。