ScalaでRTFをパースする.

会社でたまたまRTFからテキストを抽出しなければならない問題に遭遇しました.RTFというのは私は専門家ではないのですが、実は付き合いだけは長くて、10年前くらいにWordのバイナリーとつき合わせて必死に解析した覚えがあります.

WEBを見るとRTF⇒テキスト、テキスト⇒RTFというようなツールはフリーで公開されています.そちらを使ってしまえばわけはないのですが、たまたまScalaで書いたらどうするのか?と疑問が湧いて調べてみました.

対象とするRTFは例えば次のようなUnicode文字列を含んだものをです.

{\rtf1\ansi\ansicpg932\uc1\deff0 {\fonttbl{\f0\fswiss\fcharset128 \'54\'61\'68\'6F\'6D\'61{\*\falt \'54\'61\'68\'6F\'6D\'61};}}{\colortbl\red0\green0\blue0;}{\stylesheet{\s0\f0\fs24\cf0\sbasedon222\snext0\ql Default;}}{\*\generator Adobe Acrobat Reader 11.0.7}{\info{\creatim\yr2014\mo8\dy1\hr14\min28\sec46}{\revtim\yr2014\mo8\dy1\hr14\min28\sec46}{\id3661956}}
\pard\plain\ql\f0\fs24\cf0 \paperh14570\paperw10318\margt900\margb170\margr900\margl900\pghsxn15070\pgwsxn10318\margtsxn1167\margbsxn229\margrsxn338\marglsxn442\pard\par\pard\b\fs36 \u3587 \'2E\u3640 \'2E\u3655 \'2E\u3616 \'2E\u3638 \'2E\u3628 \'2E\u3611 \'2E\u3640 \'2E\u3658 \'2E\u3652 \'2E\u3595 \'2E\u3658 \'2E
\b0\b\fs36 \u3648 \'2E\u3611 \'2E\u3611 \'2E\u3628 \'2E\u3628 \'2E\u3608 \'2E\u3653 \'2E\u3622 \'2E\u3608 \'2E\u3661 \'2E\'20\b0\b\fs36 \u3595 \'2E\u3657 \'2E\b0\b\fs36 \u3624 \'2E\u3619 \'2E\u3652 \'2E\u3628 \'2E\u3658 \'2E\u3587 \'2E\u3641 \'2E\u3604 \'2E\u3652 \'2E\u3595 \'2E\u3658 \'2E
\b0\b\fs36 \u3592 \'2E\u3635 \'2E\u3608 \'2E\u3649 \'2E\u3587 \'2E\u3618 \'2E\u3638 \'2E\u3655 \'2E\'20\u3628 \'2E\u3592 \'2E\u3628 \'2E\u3617 \'2E\u3655 \'2E\u3635 \'2E\u3592 \'2E\'20\u3587 \'2E\u3641 \'2E\u3658 \'2E\u3618 \'2E\u3587 \'2E\u3655 \'2E\u3636 \'2E\u3608 \'2E\u3638 \'2E\u3657 \'2E\u3625 \'2E\u3641 \'2E\u3605 \'2E\'20
\b0\ulnone\strike0\par\pard\par\pard\par\pard\par\pard\b\fs20 \u3606 \'2E\u3634 \'2E\'20\u3624 \'2E\u3595 \'2E\u3657 \'2E\b0\b\fs20 \u3624 \'2E\u3619 \'2E\u3589 \'2E\u3658 \'2E\u3608 \'2E\u3628 \'2E\u3635 \'2E\u3628 \'2E\u3615 \'2E\u3655 \'2E\u3635 \'2E\u3592 \'2E\u3618 \'2E\u3624 \'2E\u3605 \'2E\u3649 \'2E\u3618 \'2E\u3654 \'2E\u3622 \'2E
\b0\b\fs20 \'21\'20\b0\ulnone\strike0\ulnone\strike0\ulnone\strike0\ulnone\strike0\ulnone\strike0 } 

RTFはもうOpen XMLが出たのであまり使うことはないでしょうけれど、それでもWindowsクリップボードにコピーすればたいてい書式付テキストはRTFです.RTFの仕様は以下から.docx形式のものをダウンロードできます.

Word 2007: Rich Text Format (RTF) Specification, version 1.9.1

RTFは結構癖があるのでいやらしいのですが、いろいろWEBをあたっていてパーサー・コンビネータ(Combinator Parsing)が良いかも?という結論になりました.Martin Oderskyの「Scala スケーラブルプログラミング(インプレスジャパン刊)」ではp.648~に載っています.

作ってみたのは次のようなRtfParserクラスです.何を見て作ったかというと上記のRTFの仕様のIntroductionのControl Word, Control Symbol, Groupの解説です.たぶん完全ではないのですがとりあえず目的のRTFで動けばよいのでちょっと適当です.

import scala.util.parsing.combinator._

class RtfParser extends RegexParsers{
  def text: Parser[Any] = "[0-9a-zA-Z.-;]+".r
  def asciiLetterSequence: Parser[Any] = "[a-z]+".r
  def delimiter: Parser[Any] = "[\t\n ]*".r|"[-]?[0-9]+".r
  def controlWord: Parser[Any] = "\\"~asciiLetterSequence~delimiter
  def controlSymbol: Parser[Any] = "\\"~"['*]".r~"[0-9a-zA-Z]*".r
  def group: Parser[Any] = "{"~rep(controlWord|controlSymbol|text|group)~"}"
}

object RtfParserTest extends RtfParser{
  val rtf="[ここにRTFを直接入れました!]"
  def main(args: Array[String]){
    println(parseAll(group,rtf))
  }
}

意味するところは、

① textは英数字と付加的な記号(適当です)
② asciiLetterSequenceは小文字の英字
③ delimiterはホワイトスペースか、数値
④ controlWordは"\"(バックスラッシュ)とasciiLetterSequenceとdelimiterの組み合わせ
⑤ controlSymbolは"\"(バックスラッシュ)と"'"か"*"とそれに数字と英字が続くもの(ちょっと適当です)
⑥ groupは"{"で始まって"}"で終わる.内容はcontrolWordかcontrolSymbolかtextか、そしてgroup自身の繰り返し.

です.これで走らせて見るとなんとちゃんとパースしてくれました.これだけのコードでは結果は非常にイマイチですが、ちゃんとパースしていることは確認できます.

イメージ 1


[1.1740] parsed: *1~})))~}), *2~}), *3~})))~}), *4~}), *5~}), *6~}), *7~})))~}), *8~})

これをカスタマイズしてそれなりの結果を得るのにはまだまだコードが必要でしょう.しかしScalaではパーサーをこんなに簡単に書けてしまうのですね.これには感激しました.F#でも外付けでこのようなパーサー・コンビネータがあるようです.時間があったらF#も試してみたいと思います.

*1:{~List(((\~rtf)~), 1, ((\~ansi)~), ((\~ansicpg)~), 932, ((\~uc)~), 1, ((\~deff)~), 0, (({~List(((\~fonttbl)~), (({~List(((\~f)~), 0, ((\~fswiss)~), ((\~fcharset)~), 128, ((\~')~54), ((\~')~61), ((\~')~68), ((\~')~6F), ((\~')~6D), ((\~')~61), (({~List(((\~*)~), ((\~falt)~), ((\~')~54), ((\~')~61), ((\~')~68), ((\~')~6F), ((\~')~6D), ((\~')~61)))~}), ;

*2:{~List(((\~colortbl)~), ((\~red)~), 0, ((\~green)~), 0, ((\~blue)~), 0;

*3:{~List(((\~stylesheet)~), (({~List(((\~s)~), 0, ((\~f)~), 0, ((\~fs)~), 24, ((\~cf)~), 0, ((\~sbasedon)~), 222, ((\~snext)~), 0, ((\~ql)~), Default;

*4:{~List(((\~*)~), ((\~generator)~), Adobe, Acrobat, Reader, 11.0.7

*5:{~List(((\~info)~), (({~List(((\~creatim)~), ((\~yr)~), 2014, ((\~mo)~), 8, ((\~dy)~), 1, ((\~hr)~), 14, ((\~min)~), 28, ((\~sec)~), 46

*6:{~List(((\~revtim)~), ((\~yr)~), 2014, ((\~mo)~), 8, ((\~dy)~), 1, ((\~hr)~), 14, ((\~min)~), 28, ((\~sec)~), 46

*7:{~List(((\~id)~), 3661956

*8:\~pard)~), ((\~plain)~), ((\~ql)~), ((\~f)~), 0, ((\~fs)~), 24, ((\~cf)~), 0, ((\~paperh)~), 14570, ((\~paperw)~), 10318, ((\~margt)~), 900, ((\~margb)~), 170, ((\~margr)~), 900, ((\~margl)~), 900, ((\~pghsxn)~), 15070, ((\~pgwsxn)~), 10318, ((\~margtsxn)~), 1167, ((\~margbsxn)~), 229, ((\~margrsxn)~), 338, ((\~marglsxn)~), 442, ((\~pard)~), ((\~par)~), ((\~pard)~), ((\~b)~), ((\~fs)~), 36, ((\~u)~), 3587, ((\~')~2E), ((\~u)~), 3640, ((\~')~2E), ((\~u)~), 3655, ((\~')~2E), ((\~u)~), 3616, ((\~')~2E), ((\~u)~), 3638, ((\~')~2E), ((\~u)~), 3628, ((\~')~2E), ((\~u)~), 3611, ((\~')~2E), ((\~u)~), 3640, ((\~')~2E), ((\~u)~), 3658, ((\~')~2E), ((\~u)~), 3652, ((\~')~2E), ((\~u)~), 3595, ((\~')~2E), ((\~u)~), 3658, ((\~')~2E), ((\~b)~), 0, ((\~b)~), ((\~fs)~), 36, ((\~u)~), 3648, ((\~')~2E), ((\~u)~), 3611, ((\~')~2E), ((\~u)~), 3611, ((\~')~2E), ((\~u)~), 3628, ((\~')~2E), ((\~u)~), 3628, ((\~')~2E), ((\~u)~), 3608, ((\~')~2E), ((\~u)~), 3653, ((\~')~2E), ((\~u)~), 3622, ((\~')~2E), ((\~u)~), 3608, ((\~')~2E), ((\~u)~), 3661, ((\~')~2E), ((\~')~20), ((\~b)~), 0, ((\~b)~), ((\~fs)~), 36, ((\~u)~), 3595, ((\~')~2E), ((\~u)~), 3657, ((\~')~2E), ((\~b)~), 0, ((\~b)~), ((\~fs)~), 36, ((\~u)~), 3624, ((\~')~2E), ((\~u)~), 3619, ((\~')~2E), ((\~u)~), 3652, ((\~')~2E), ((\~u)~), 3628, ((\~')~2E), ((\~u)~), 3658, ((\~')~2E), ((\~u)~), 3587, ((\~')~2E), ((\~u)~), 3641, ((\~')~2E), ((\~u)~), 3604, ((\~')~2E), ((\~u)~), 3652, ((\~')~2E), ((\~u)~), 3595, ((\~')~2E), ((\~u)~), 3658, ((\~')~2E), ((\~b)~), 0, ((\~b)~), ((\~fs)~), 36, ((\~u)~), 3592, ((\~')~2E), ((\~u)~), 3635, ((\~')~2E), ((\~u)~), 3608, ((\~')~2E), ((\~u)~), 3649, ((\~')~2E), ((\~u)~), 3587, ((\~')~2E), ((\~u)~), 3618, ((\~')~2E), ((\~u)~), 3638, ((\~')~2E), ((\~u)~), 3655, ((\~')~2E), ((\~')~20), ((\~u)~), 3628, ((\~')~2E), ((\~u)~), 3592, ((\~')~2E), ((\~u)~), 3628, ((\~')~2E), ((\~u)~), 3617, ((\~')~2E), ((\~u)~), 3655, ((\~')~2E), ((\~u)~), 3635, ((\~')~2E), ((\~u)~), 3592, ((\~')~2E), ((\~')~20), ((\~u)~), 3587, ((\~')~2E), ((\~u)~), 3641, ((\~')~2E), ((\~u)~), 3658, ((\~')~2E), ((\~u)~), 3618, ((\~')~2E), ((\~u)~), 3587, ((\~')~2E), ((\~u)~), 3655, ((\~')~2E), ((\~u)~), 3636, ((\~')~2E), ((\~u)~), 3608, ((\~')~2E), ((\~u)~), 3638, ((\~')~2E), ((\~u)~), 3657, ((\~')~2E), ((\~u)~), 3625, ((\~')~2E), ((\~u)~), 3641, ((\~')~2E), ((\~u)~), 3605, ((\~')~2E), ((\~')~20), ((\~b)~), 0, ((\~ulnone)~), ((\~strike)~), 0, ((\~par)~), ((\~pard)~), ((\~par)~), ((\~pard)~), ((\~par)~), ((\~pard)~), ((\~par)~), ((\~pard)~), ((\~b)~), ((\~fs)~), 20, ((\~u)~), 3606, ((\~')~2E), ((\~u)~), 3634, ((\~')~2E), ((\~')~20), ((\~u)~), 3624, ((\~')~2E), ((\~u)~), 3595, ((\~')~2E), ((\~u)~), 3657, ((\~')~2E), ((\~b)~), 0, ((\~b)~), ((\~fs)~), 20, ((\~u)~), 3624, ((\~')~2E), ((\~u)~), 3619, ((\~')~2E), ((\~u)~), 3589, ((\~')~2E), ((\~u)~), 3658, ((\~')~2E), ((\~u)~), 3608, ((\~')~2E), ((\~u)~), 3628, ((\~')~2E), ((\~u)~), 3635, ((\~')~2E), ((\~u)~), 3628, ((\~')~2E), ((\~u)~), 3615, ((\~')~2E), ((\~u)~), 3655, ((\~')~2E), ((\~u)~), 3635, ((\~')~2E), ((\~u)~), 3592, ((\~')~2E), ((\~u)~), 3618, ((\~')~2E), ((\~u)~), 3624, ((\~')~2E), ((\~u)~), 3605, ((\~')~2E), ((\~u)~), 3649, ((\~')~2E), ((\~u)~), 3618, ((\~')~2E), ((\~u)~), 3654, ((\~')~2E), ((\~u)~), 3622, ((\~')~2E), ((\~b)~), 0, ((\~b)~), ((\~fs)~), 20, ((\~')~21), ((\~')~20), ((\~b)~), 0, ((\~ulnone)~), ((\~strike)~), 0, ((\~ulnone)~), ((\~strike)~), 0, ((\~ulnone)~), ((\~strike)~), 0, ((\~ulnone)~), ((\~strike)~), 0, ((\~ulnone)~), ((\~strike)~), 0