JavaCCで日本語を扱う方法

これは、沖電気の村田さんがJavaHouse MLに投稿された記事を元に修正して寄稿してくださった情報です。


村田@沖電気.関西研です。

> > 日本語を含んだ文字列を扱える
> > parserをJavaCCで記述している方にお伺いしたいのですが、どのよ
> > うな方法で日本語処理の問題を対処されていますか。
> 
> options {
> 	JAVA_UNICODE_ESCAPE = true;
> }
> にしておいて、native2ascii を通して読み込むと言う方法を
> 使っています。

これが普通だと思います。

でも、非常にタイムリーなことに、昨日、ようやく、JavaCC で JDK1.1.x の Readerクラスが使えるようになりました。

以下、ちょっと長いですが。

まず、JavaCC の文法で日本語などのASCII以外の文字を扱うためには、native2ascii などを使って、Java Unicode Escape(\uxxxx) に変換する必要があります。

例)
% native2ascii Test.euc.jj Test.jj

次に、日本語などのASCII以外の文字を含んだテキストをparsing する方法は、3通りあります。

  1. 入力ファイルを Java Unicode Escape に変換する

    解析したい入力ファイルをあらかじめ文法ファイルと同じように native2ascii で変換します。

    さらに、文法ファイルの方では、オプションで、

    		JAVA_UNICODE_ESCAPE=true;
    
    を指定します。

    これで JDK1.0.2,JDK1.1.x 上で動作するパーザができあがります。

  2. 入力ファイルを Unicode に変換する

    解析したい入力ファイルをあらかじめUnicode のファイルに変換します。

    	% native2ascii inputfile | native2ascii -reverse -encoding UnicodeBigUnmarked > inputfile.unicode
    

    さらに、文法ファイルの方では、オプションで、

    		UNICODE_INPUT=true;
    
    を指定します。

    これで JDK1.0.2,JDK1.1.x 上で動作するパーザができあがります。

  3. JDK1.1.x の Reader クラスを使う

    1, 2の方法では、入力ファイルの変換が必要になって、少し手間です。 JDK1.1.x で実行するなら Reader クラスが使えます。 このやりかたでやっと昨日うまく動作したところだったのです。

    まず、JavaCC の最新版(注: このドキュメントは0.7pre5をベースにしている)を http://www.suntest.com/JavaCC/ から取り寄せてインストールします。

    まず、以下のオプションを指定して、UCode_CharStream.java を生成させます。

    		UNICODE_INPUT=true;
    		STATIC=true;		←これは任意。
    

    そして、このファイルを修正するのでファイル名を変更します。

    	% mv UCode_CharStream.java UCode_Char_CharStream.java
    

    このファイルを修正します。

    1. CharStream を implements するようにする。
    2. すべての byte 配列を char 配列に変更します。
    3. すべての InputStream の変数、型を Reader に変更します。
    4. FillBuff()にバグがあるので、サンプルのように ""throw e;" を catch ブロックの最後に追加します(注: これは0.7pre5の正式版では修正済)。

    この修正を行った UCode_Char_CharStream.java のサンプルを、このメールの最後につけておきます。

    文法ファイルは、

    		USER_CHAR_STREAM=true;
    		UNICODE_INPUT=true;
    		STATIC=true;		←これは任意。
    
    のオプションをつけて、パーザの起動のところは例えば以下のようにします。
    	PARSER_BEGIN(TestJavaCC)
    
    	import java.io.*;
    
    	public class TestJavaCC {
    	    public static void main(String args[]) {
    		TestJavaCC reader;
    		if (args.length != 1) {
    		    System.out.println(
    			"Usage : java TestJavaCC inputfile");
    		    return;
    		}
    
    		try {
    		    reader = new TestJavaCC(
    			new UCode_Char_CharStream(
    			    new InputStreamReader(
    			    new FileInputStream(args[0]), "JISAutoDetect"),
    			0, 0));
    		    reader.Start();
    		} catch (FileNotFoundException e) {
    		    System.out.println("TestJavaCC: File "
    				       +args[0]+" not found.");
    		    return;
    		} catch (UnsupportedEncodingException e) {
    		    System.out.println("TestJavaCC: UnsupportedEncode.");
    		    return;
    		} catch (ParseError e) {
    		    System.out.println("TestJavaCC: File "
    					+args[0]+" couldn't be parsed.");
    		    return;
    		}
    	    }
    	}
    
    	PARSER_END(TestJavaCC)
    
    これでもう一度 JavaCC を起動すれば、JDK1.1.x で動作するパーザができあがります。

ぼくは、入力ファイルがそのまま使える3を使っています。 JavaCC も version 0.8 で Reader が標準で使えるようになるようです。 (JDK 1.1 にスイッチすると言っています。)

--
                                     --------------------------------------
                                     村田稔樹@沖電気工業(株)関西総合研究所
                                                      mura@kansai.oki.co.jp
                                     --------------------------------------

村田%JavaCCユーザ@沖電気.関西研です。

> 現在、JavaCC(0.8pre1)とそのexampleについてくる。Java1.1.jj
> (Java1.1の文法ファイル) を利用したプログラムを作成しています
> が、問題を見つけたので相談させてください。
:
> [原因]
> JavaCCを通した後に出来るASCII_UCodeESC_CharStream.java
> 中のreadCharメソッド中の10行目あたりに次のような条件文があります。
> 
>     if (((buffer[bufpos] = c = (char)((char)0xff & ReadByte())) ==
> '\\'))
:
> [質問]
> 
> 質問は、自分はこの行がバグだと思うのですが、本当にそうなのか?

はい、バグです。 これを直したパッチをこのメールの最後につけておきます。

かなり前につくったパッチで、JavaCC のメーリングリストには流したのですが、このMLには流してなかったような気がします。 (流しておくべきでした。すみません。)

# 風間さんへ、
# http://www.ingrid.org/java/javacc/ のも
# このメールに更新しておいてください。

JavaCC が生成する

    ASCII_UCodeESC_CharStream.java
    UCode_CharStream.java
    UCode_UCodeESC_CharStream.java
の3つのソースに対するパッチです。

これらのソースは JavaCC に対するオプションの組合せで、このうちの1つだけが生成されるものですので、必要なものを用いてください。

また、SJIS の Javaソースを JavaCC で解析したいときは、JavaCC.jj で、まずオプションを

options {
    UNICODE_INPUT=true;
    JAVA_UNICODE_ESCAPE=true;
}
というように UNICODE_INPUT を true にして、
      parser = new JavaCCParser(System.in);
としているところを(デフォルトエンコーディングを用いるなら)
      parser = new JavaCCParser(new InputStreamReader(System.in));
または、明示的には
      parser = new JavaCCParser(new InputStreamReader(System.in, "SJIS"));
というようにする必要があります。 (UnsupportedEncodingExceptionもcatchして。)

コンストラクタに Reader が来るならUNICODE_INPUT=true が不要な気もしますが、たしか、これをしないと内部で生成される状態遷移表が1byte文字にのみ対応してしまっていたような気がします。

このパッチは JavaCC 0.8pre1 用に作ったものですが、0.8pre2 でもそのまま使えます。

--
                                     --------------------------------------
                                     村田稔樹@沖電気工業(株)関西総合研究所
                                                      mura@kansai.oki.co.jp
                                     --------------------------------------

村田@沖電気.関西研です。

武川@富士ソフトABCさん:
> ところで、また別にひっかかった部分があったので
> メールします。
:
> 今回、村田さんに教えられたオプションで生成される 
> UCode_UCodeESC_CharStream.java ですが、
> どうもReInitメソッドに対応していないようです。
> 
> 具体的には、
>   public void ReInit(java.io.Reader dstream,
>                  int startline, int startcolumn, int buffersize)
> 
> で、フラグ関係の初期化を行っていません。
> ASCII_UCode_UCodeESC_CharStream.java
> の同じメソッド見比べてみてください。
> 
> というわけで、二つ目のファイルを読込むと
> 一つ目のファイルの次の位置を見にいって
> 例外が発生してしまうようです。

ReInit は使ってませんでしたので、このバグは知りませんでした。

おっしゃるように ASCII_UCodeESC_CharStream.java の ReInit のフラグ関係の初期化を UCode_CharStream.java と UCode_UCodeESC_CharStream.java に コピーしたものの最終的な JavaCC 0.8pre1(or pre2)に対するパッチを 再度添付しておきます。

ありがとうございました。

> >>>コンストラクタに Reader が来るなら
> >>>UNICODE_INPUT=true が不要な気もしますが、
> >>>たしか、これをしないと内部で生成される状態遷移表が
> >>>1byte文字にのみ対応してしまっていたような気がします。
> 
> よくわからないのですが、状態遷移表が1バイト文字のみ
> だとどのような問題があるのでしょうか?

中身の詳細まで見ていないのでよくわかりませんが、 おそらく入ってきた文字によって遷移する先を決定する表が char の下位8ビットだけで作られていて、 比較も下位8ビットだけで行なうのでしょう。

ということは、JavaCC の Lexicalルールに ASCII 以外の文字が入っていると間違った遷移を することがあることになりますね。 (想像上です。たぶん。。。)

> 今回はJavaのソースファイルを入力としているのですが、
> 日本語はコメント部分のみなので、特に気にしなくても
> よいと考えていました。

下位8ビットが */ となる日本語2文字があれば おかしくなる可能性がありますね。 (そんな文字があるかどうかは知りませんが。。。)

また、日本語はクラス名などにも仕様上は使えますので、 どんな Javaソースファイルでも正しく動作するためには

UNICODE_INPUT=true
にすべきですね。

まあ、とりあえず UNICODE_INPUT=true が無難ですね。 別にパフォーマンスにそんなに違いはないでしょうし。

どうせなら UNICODE_INPUT というオプションはなくしてしまって どんなときでも UNICODE_INPUT=true になるようにしたらって JavaCC の作者に提案したんですが、 たしか却下されたような覚えがあります。 まあちょっとでもパフォーマンス(空間、速度)がいい方がいいのでしょう。

--
                                     --------------------------------------
                                     村田稔樹@沖電気工業(株)関西総合研究所
                                                      mura@kansai.oki.co.jp
                                     --------------------------------------
ReInit も直した新しいパッチ