技術をかじる猫

適当に気になった技術や言語、思ったこと考えた事など。

ScalaのRegexParser使ってみた

RegexParser って何よ?

Scalaのパーサコンビネーターと呼ばれる字句解析ライブラリのうちの一つ。 他に派生クラスで JavaParser とか、基底で Parser とかがあります。

字句解析っていってピンとくる人はきっと大学とかでコンパイラの授業受けた人ですね。

特定の構文で書かれた文書を分解して、要素ごとの情報に分解する操作をこう言います。 ソースコードとかの命令とかリテラルとかを認識して、全体の論理データに落とすとかそういう使い方をします。*1

どんな時に使えるの?

例に挙げましたがコンパイラとか、インタプリタの実装とかで使います。 結構きちっとした構文を持ってる言語なら、案外さっくり書けたりします。

私の場合だと、仕事の資料でグラフ作る時にGraphvixをたまに使うのですが、ここから特定範囲のデータを抽出して別のグラフを作ったり、複数のグラフをまとめたりなんてことをこれで作ったことがあります。*2

まぁそれも、リテラルや構文がキッチリ決まってるからできる事なんですけどね,,,,

使い方は?

このへんでやってくださってるのが参考になるはずです。

ハマったところ

(注意:上記のサイトで表示している使い方が分からないと下記は意味不明です) 何をしようとしたかというと、PukiWiki をパースできないかなと思ったわけです。

で、何が挫折したのかというと、RegexParser は行単位でパースするのですが、その中の要素定義に.*とか1個置いてしまうと、行末まで問答無用で吸収してしまう。 逆に.*?なんて定義しようものなら1文字しか拾ってくれないという虐めっぷりでした。

たとえばこんな定義を書くと失敗します

  def string = """.*""".r

  def italic = "'''" ~> string <~ "'''"

  // これでは '''こんなメッセージ''' をパースすることはできません

これは、string 定義が後続の ''' を食い殺してしまうため、まともにパースできません。 明確に認識するためには下記のように書くしかありません。

  def string = """.*""".r

  def italic = "'''(.*)'''"

すると今度は ^^ という関数を使ったときに困るのと、1行単位に解析しているので、複数行にまたがるとアウトであるということでした

'''これは
ダメ'''

参ったネ。

結論、構文とか、文法、リテラルや命令文がふんわりした系のマークアップでやろうとすると地獄。 その手のものは多分Char単位でパース可能な Parser クラスでやった方がいい。

今日できたところまで晒します

PukiWiki のLine要素*3をこれでどうにかパースできました。

え?複数行に分けて書かれたらどーすんだって?知らねーよ(´Д⊂ヽ

import scala.util.parsing.combinator._

class LineElement

case class LineBreak() extends LineElement

case class Str(body:String) extends LineElement

case class Italic(body:LineElement) extends LineElement

case class Bold(body:LineElement) extends LineElement

case class Sized(size:Int, child:LineElement) extends LineElement

case class Colored(color:String, child:LineElement) extends LineElement

case class Line(elements:List[LineElement]) extends LineElement

object PukiWikiParser extends RegexParsers {

  def string:PukiWikiParser.Parser[LineElement] = """.*""".r ^^ {Str}

  def italic:PukiWikiParser.Parser[LineElement] = "'''(.*)'''".r ^^ { res => Italic(parseChild(res.substring(3, res.length - 3))) }

  def bold:PukiWikiParser.Parser[LineElement] = "''(.*)''".r ^^ { res => Bold(parseChild(res.substring(2, res.length - 2))) }

  def sized:PukiWikiParser.Parser[LineElement] = """&size(""" ~> "\\d+".r ~ ")" ~ """\{(.*)\};""".r ^^ { res => Sized(res._1._1.toInt, parseChild(res._2.substring(1, res._2.length - 2))) }

  def colored:PukiWikiParser.Parser[LineElement] = """&color(""" ~> "[a-z\\d, ]+".r ~ ")" ~ """\{(.*)\};""".r ^^ { res => Colored(res._1._1, parseChild(res._2.substring(1, res._2.length - 2))) }

  def childLine:PukiWikiParser.Parser[LineElement] = colored | sized | italic | bold | string

  def parseChild(target:String) = parse(childLine, target).get

  def forceLb = """(.*)&br;""".r ^^ {res => Line(List(parseChild(res.substring(0, res.length - 4)), LineBreak()))}

  def lb = "(.*)~".r ^^ {res => Line(List(parseChild(res.substring(0, res.length - 1)), LineBreak()))}
}

*1:因みにコンパイラだとこの後構文解析とか入りますが、それは別な話で、、、w

*2:慣れれば半日位で両方の機能が作れるようになります

*3:の一部