最近は、XMLの普及によって、パーサージェネレータの出番はめっきり少なくなりましたが、ちょっとした独自形式ファイルの読み取りや、数式の解釈にはまだまだ役に立つツールです。ここでは、自分が引っかかったケースや、おそらく他の人も役に立つメモを適当に綴っていきます。この方面の専門家ではないので間違いなどありましたらお知らせ下さい。
コンパイラーのような、ある決まった形式のファイルを読んで、何かをするプログラムを自動生成するツール。コンパイラーを作るためのコンパイラー。単純作業だけど作ると大変な「パースする(文を読んで解釈する)プログラム」を作ってくれるソフト。<(説明も難しかったり。)
開発元:JavaCC@java.net
JavaTipやSmartDocで有名な浅海さんのページが参考になります。
このような時には手軽にプログラムを書くことが出来ます。
JavaCC文法があれば、幸せな人が増えるみたいです。
ファイルや文字列を読んで解釈するプログラムを書きたいときに、このようなツールを使うわけですが、自動生成だけに次のようなケースには使いづらかったりします。
私の経験上、以上のケースに一致する場合は、JavaCCを使って文法解釈するのに大変苦労します。苦労しないことがJavaCCの存在意義だとすれば、別の方法を考えることも検討した方がいいかもしれません。
まずJavaCCを使う上でやることは、トークン(Token)と呼ばれる文法の最小要素を定義することです。(JavaCCによって生成された文法解釈プログラムは、入力された文書を、このトークンという単位に分解してから解釈を始めますので、これが無ければ始まりません。)
トークンには以下の4種類ありますが、普通は上の2つだけで大丈夫です。
トークンの例などはJavaCCに付属のサンプルにたくさん付いています。大変参考になります。
JavaCCのサンプルにある、JavaGrammarを参考にして、
< #LETTER: //日本語を含んだ文字 [ "\u0024", "\u0041"-"\u005a", "\u005f", "\u0061"-"\u007a", "\u00c0"-"\u00d6", "\u00d8"-"\u00f6", "\u00f8"-"\u00ff", "\u0100"-"\u1fff", "\u3040"-"\u318f", "\u3300"-"\u337f", "\u3400"-"\u3d2d", "\u4e00"-"\u9fff", "\uf900"-"\ufaff" ] >
を使うとか、あるいは単純に、
< #NONASCII: ["\u0080"-"\ufaff"] >
を使うという手もあるかと思います。なお、日本語を入力するには、後述の細工を行う必要があります。
いろいろあるかと思いますが、ここではTeXの数式の例を出します。
TeXの数式を解釈する場合、「xy_123^45\beta」は「 x | y | _ | 1 | 23 | ^ | 4 | 5 | \beta 」と分解する方法が自然です。
しかし、普通に文法を考えると、「xy | _ | 123 | ^ | 45 | \beta」と文字が続いてしまい、全然意味の違うものとして解釈されてしまいます。 それではと、単語や数字を1文字単位で区切ってしまうと、「12.34」や、「\text{this is a pen.}」などの別の個所で問題が起きてしまう上に、誰も1文字単位で文法を考えたくありません。
これを解決するには、「x y_1 23^4 5\beta」のように、入力文字列の適当な個所に空白を入れます。つまり、JavaCCによる文法解釈プログラムに入れる前に、自前のプリプロセッサによる前処理を行うということです。これは文法解釈よりも簡単ですから、ちょっと頑張れば解決します。しかし、あまり複雑な前処理をやってしまうと、何のためのJavaCCか分かりませんから、JavaCCの作業との兼ね合いで一番簡単になる解を見つけることが重要です。
前は、StringBufferInputStreamというクラスがあったのですが、今は Deprecated なので自分で作ります。単に、InputStreamでStringReaderをラップするだけです。
public class StringInputStream extends InputStream { StringReader in; private StringInputStream() {} /** build input stream from given string. * @param source input stream source */ public StringInputStream(String source) { in = new StringReader(source); } public int read() throws IOException { return in.read(); } public void close() throws IOException { in.close(); } public synchronized void mark(int readlimit) { try { in.mark(readlimit); } catch(IOException e) { throw new RuntimeException("IOException : StringInputStream["+ toString()+"]"); } } public synchronized void reset() throws IOException { in.reset(); } public boolean markSupported() { return true; } }
JavaCCの文法解釈クラスのコンストラクタで、以下のように与えれば動きます。
public MyParser(String in) throws ParseException { this(new StringInputStream(in)); }
JavaCCは最初のバージョンから日本語が通りませんでした。途中でユニコードの入力もOKになったはずですが、バグがあってしかも未だに直っていません。日本語を通す方法として、以下の方法があります。
以下のように、文字コードが128以上の文字は、ユニコードエスケープにしてしまいます。これを上の「StringInputStream」クラスに適当に加えて変換を行わせます。
public static String escape(String in) { StringBuffer buf = new StringBuffer(); for (int i=0;i<in.length();i++) { int code = (int)in.charAt(i); if (code >= 128) { buf.append("\\u"+Integer.toHexString(code)); } else { buf.append(in.charAt(i)); } } return buf.toString(); }
以下の2つのスイッチを設定します。
UNICODE_INPUT=false; JAVA_UNICODE_ESCAPE=true;
これで、日本語は自動的にASCIIコードによるエスケープに置き換わって入力されますので、日本語を含んだ文章を解釈できます。動作は遅いかもしれませんが、自前の解釈プログラムを作るよりはずっと時間の短縮になります。
なお、Javaのプログラムについて一般的に言えることですが、全角の「〜」などの文字はユニコードとの変換の過程で化ける可能性があります。
SPECIAL_TOKEN を使う方法がありますが、ここではJAVA_CODEを使ってトークンを自分で判断してスキップしてしまうケースについて説明します。TeX の数式でのテキスト部分の解釈の例を挙げます。
「\text{This is a pen.}」を解釈します。ここでは「| \text | { | This is a pen. | } 」と解釈したいとします。「\text」を見つけた時点で次のメソッドを呼びます。
JAVACODE String contents() { Token curTok = token; Token tok = getNextToken(); // 次のトークンを読む if (!(tok.kind == LBRACE)) { // LBRACE = "{" token = curTok; // 左カッコではなかったら return null; // トークンを一つ前に戻して戻る } int nesting = 1; StringBuffer content = new StringBuffer(); while (true) { tok = getNextToken(); if (tok == null) break; if (tok.kind == RBRACE) { // RBRACE = "}" nesting--; if (nesting == 0) { break; } } if (tok.kind == LBRACE) { nesting++; } content.append( tok.image ).append(' '); // トークンの切れ目に空白を入れる // (ここは各適用場面によって考える必要がある) } return content.toString().trim(); }
帰り値として、「{ }」で囲まれた部分が文字列として帰ってきます。複数回入れ子になっていても大丈夫です。(これが正しいJAVA_CODEの使い方か良く分かっていませんが・・・)
桜井雅史: | E-mail : m.sakurai@cmt.phys.kyushu-u.ac.jp Web page : http://www.cmt.phys.kyushu-u.ac.jp/~M.Sakurai/ |