「Javaの哲学」のページへ
updated 21 Sep. 2007

「Javaによるテキスト処理入門」の恥かき

Dogs&Cats里親募集HPへ

★2006年より、Javaプログラミング関連記事の新規掲載場所を[Javaの手帖]に統一しました。


What's new on this update

050307: GNOMEのUTF-8化でJavaのアプリケーションをこんなふうにリライト。

050113: p134, 2行目のTreeNodeは、正しくはDefaultMutableTreeNodeです。add()メソッドは、後者にしかありません。

040317: p272のlookahead(直後の条件を指定)の説明を、おそまきながら[補足]いたします。指定の位置によって、意味が違うんです!。

040115: p133に関する[訂正]。ツリーのノードの画像は、簡単に変えられるのだった!。

031109: Tokenを下痢してしまったとんでもないエラー
030809: JScrollPaneに収めたJTextAreaを自動(not手動)スクロールさせるには?。ページ内リンク
030722: Java World(*)の初心者向けフォーラムが、6月下旬に新装再開店しました。Java Beginnerと、Theory & Practiceを訪ねてみましょう。質問者の多くがインド人または中国人、日本人は皆無、というところがおもしろいですねー。日本の未来を、暗示しているようですねー。

0. 正誤訂正

  1) p39上2
  【誤】(たとえばLinux上のVim)
  【正】(たとえばVim)
  【解説】Windows上のVimで文字コードの変換を可能にするためには、gvim.exeが存在するディレクトリ内(フォルダ内)に、iconv.dllを置いてください。Windows用のVim本体は、http://www.vim.org/ → Download → PC: MS-DOS and MS-Windows → gvim61.exeをダウンロードし(*)、“実行”するとインストールされます。iconv.dllは、同じダウンロードページからダウンロードできます。(*:2002/9月現在。バージョンアップ後は、gvim62.exeとなるでしょう。)
  Windows上でVimを使っていて、日本語テキストファイルをEUC-JPの文字コードで保存するためには、Vimのコマンドモードで

    :set fileencoding=euc-jp

とコマンドし、:wコマンドまたは「保存」「名前を付けて保存」メニューを実行します。
  あるいは、これからいよいよファイルをセーブする(保存する)、という時点で:

    :w ++enc=euc-jp filename

とコマンドします(ファイルがfilenameという名前でセーブされます)。
  Windows上のVimがEUC-JPのテキストをロードすると、画面の文字がおかしくなりますから、次のようにコマンドします:

    :e ++enc=euc-jp

そして、セーブ(保存)時には、上の例のように、:wコマンドに++enc=euc-jpまたは++enc=sjisオプションを与えます。
  より詳しい情報は、Vimのドキュメントの内、とくに、editing.txtやmbyte.txtを読んでみてください。

1.なぜ8ビット/1バイトが標準単位なのか?

  11ページのコラムは、正しい歴史的情報ではありません。正しい情報は、ここにあります。昔々のコンピュータ業界はIBMの勢力が絶大でしたから、IBMの方式がそのまま、業界標準になってしまったのであります。

2.JpSubjectsAnalyzerを試用するためのサンプルファイル

  249ページのJpSubjectsAnalyzerプログラムを試用してみるためには、日本語の、かなり長いテキストファイルが入力ファイルとして必要です。手元にそんなファイルが現時点でないかたは、このファイル(使用文字コードはShift_JIS)をダウンロードして使ってください。EUC-JPのファイルが必要なかたは、こちらをダウンロードしてください。ファイルの内容は、本書第6章の本文の全文です。

3.JpSubjectsAnalyzer.javaのバグ

  JpSubjectsAnalyzer.javaのtrimHead()メソッド(71行目)にバグがあります。すなわち、while節中のチェック条件の冒頭で、変数offsetの値がbuf.length()よりも確実に小さいことを検査しないと、StringIndexOutOfBoundsExceptionが投げられてしまうことがあります!。

【現状(間違っている姿)】
  /* 語頭のスペース文字等をトリミングする */
  String trimHead(StringBuffer buf){
    char ch;
    int offset = 0;

    while ((ch = buf.charAt(offset)) == ' '
         || ch == '・'){ //'・'はUnicodeではKATAKANAなのだ!
      ++offset;
    }
    return (buf.substring(offset));
  }

【正しい姿(デバッグ後)】 
  /* 語頭のスペース文字等をトリミングする */
  String trimHead(StringBuffer buf){
    char ch;
    int offset = 0;
    /*必ず最初にインデクス変数の値をチェックすること!*/
    while ((offset < buf.length()) && ((ch = buf.charAt(offset)) == ' '
     || ch == '・')){       //'・'はUnicodeではKATAKANAなのだ!
      ++offset;
    }
    return (buf.substring(offset));
  }

4. テキスト処理のデバッグの盲点--データのバグ

  最近、泥沼のように長時間手こずったデバッグ経験からの教訓を、ご報告します。
  その経験の概要は、「データにバグがあるのにプログラムにバグがあるとばかり思いこんでいた」ことです。
 いわゆるCSV(comma separated values)の1レコードは、こんな形をしています:

    data1,data2,data3,data4

  ところが、何かの間違いで、一つだけ、こんなレコードがデータファイル内にありました。

    dataM,dataN,dataO,dataP,

  Stringクラスのさまざまなメソッドを駆使して構成されているJavaプログラムの中では、この不正な形式のレコードの、最後のデータ項目(レコードの末尾のデータアイテム)は空文字列(長さがゼロのStringオブジェクト)となり、空文字列を予期していないプログラムの随所に異状を出現させます(最も多いのはStringIndexOutObBoundsExceptionを伴う異状)。
  私は最初ずっと、自作のプログラム側のバグだと思っていましたから、デバッグにたいへん苦戦しました。処理の途中結果を一覧するためのSystem.out.println();文を挿入してみて、生成されるはずのない空文字列の生成に気づき、データ側のバグを発見しました。
  【教訓】データを生成するプログラムも、データを処理するプログラムも、必ずデータ形式の正しさをチェックせよ!。前者では生成直後、後者では処理着手直前に。不正なデータを検出したときの対応は、そのプログラムの目的次第です。

##データのバグ, Part II
  平仮名をローマ字に変換するメソッドを書いたんですが、そのためのデータ構造で:

    String hira = "あいうえおかきくけこさし...";
    String[] roma = {"a", "i", ..., "ka", "ki", ...};

とやって、原文の各文字をhira.indexOf()取って、その値でromaをインデクシングするまではよかったんですが、hiraの中で、"だぢづでど"であるべきが、なぜか"たぢづでど"になっていました。つまり、'だ'がどこにもないので、hira.indexOf('だ')でromaをインデクシングするとStringIndexOutOfBoundsExceptionが投げられてしまいます。
  英語だけでなく、日本語でもミスタイプがあるので、こんなことが起きてしまいます。お粗末の一席。

5.ゲーム設計と言葉の処理

  ゲームのおもしろさの中心的要素は、ストーリーとシステムの静的動的構造だ。だからテキスト処理の課題としては、ゲームのロジックプロトタイプの制作と売り込みがある。みなさま、原作者印税を稼いでください。
  しかし本格的な課題は、テキストオンリーのゲームの高度化した復活だ。もちろんネットワークゲームだが、リアルタイムでなく非同期通信型(MUDやチャット型でなくメール型)が良い。Javaには非同期ネットワーク通信のためのインフラがすでに十分にある。世界を巻き込みたいので、やはり英語を使うゲームが良いでしょう。しかもインタフェイスをGUIにすれば、昔のテキストゲームでは不可能だった複雑なロジックも可能になるだろう。

6.日本語文字列とLinux vs. Windows

  「一度書けばどこでも動く」がJavaの宣伝文句ですが、日本語文字列を含むソースファイルは、Linux用(EUC-JP)とWindows用(Shift-JIS)の2セットを用意しなければならないケースが多くて、面倒でした。最近私は、この問題を次の“方針”で回避するようにしました:

(1)コメントはすべて英語で書く。
(2)文字列リテラルはソース中に直接書かず、ファイルから読むようにする。
  そして、そのファイルをEUC版とShift-JIS版の2とおり用意し、2つをアプリケーションに同梱します。
  どちらのファイルを読むかは

  if (System.getProperty("file.encoding").startsWith("EUC")){

などで判断します。

  システムプロパティは、こんなプログラムを使って確認してください。

7.正規表現とJavaの文字列

  構文要素としてバックスラッシュを含む正規表現を、JavaのStringリテラルとして書くときには、そのバックスラッシュ文字を2つの連続するバックスラッシュで書き表さなければなりません。このことが、思わぬ難関を招くことがあります。「バックスラッシュ文字の直後に数字が1つ以上ある」というパターンは、Javaの正規表現では\\\d+と書きますが、これをJavaのStringリテラルではどう書くのでしょう?。以下のプログラムを完成させてみて、“なぜこう書かなければならないのか”という理由を考えてみましょう:
/***************************************/
/* splittest.java */
import java.util.regex.*;

public class splittest{
  public static void main(String[] args){
   //Vimなどで使われるマッチ結果参照置換文字列
    String target = "\\2abc\\51def\\2ghi\\1\\3";
    String regx = ""; //このリテラルを正しく書いてください
    String[] results;

    System.out.println("\\2".matches(regx)); //regxが正しければtrue
    
    System.out.println("-----------");
    results = target.split(regx);
    for (int i = 0; i < results.length; ++i){
      System.out.println(results[i]);
    }
    System.out.println("-----------");
    Pattern pat = Pattern.compile(regx);
    Matcher mat = pat.matcher(target);
    while (mat.find()){
      System.out.println(mat.group());
    }
  }
}
/***************************************/

8.エンコーディング(文字コード)の指定とPrintStream

  テキストファイルの入出力で、ネイティブのエンコーディング(Linux上ならEUC-JP, Windows上ならShift_JIS)以外の文字コードを扱うときには、それらを明示的に指定しなければなりません。たとえばこんなふうに(UTF-8で書かれたファイルを読み、処理結果をUTF-8で出力する場合):

      PrintWriter pw = new PrintWriter
        (new OutputStreamWriter
         (new FileOutputStream("testoutput.txt"), "UTF-8"));
      BufferedReader br = new BufferedReader
        (new InputStreamReader
         (new FileInputStream("testinput.txt"), "UTF-8"));

ところが、こういう場合のOutputStreamWriterのコンストラクタへの第一引数として、PrintStream、典型的にはSystem.outを渡すと、文字コードが正しく処理されません。現在の私の単なる想像では、JDK 1.1以降のPrintStreamは、ネイティブの文字コードとの結びつきが特別に強くされているからではないか、と思われます。
  Javaがサポートしているエンコーディングを知るためには、こんなプログラムを動かしてみてください。

9.JScrollPaneに収めたJTextAreaの自動スクロール

  JTextAreaを表示用に使って、次々と行をappend()していくと、画面の高さを超えた部分が自動スクロールしてくれません。この問題を解決するには:

    2行目を上辺とする新しいRectangleを引数として、
    JTextAreaのscrollRectVisible()をコールします。

  旧Rectangleは、getVisibleRect()で得ます。このRectangleのyフィールドに、1行の高さを足します。行高は、JTextAreaをあなたがextendsしたクラスの中で、getRowHeight()…protectedメソッドだ!…を呼んで得ます。

  サンプルプログラムを見てください。

  この方法は、JTextArea以外のコンポーネントの自動スクロールにも応用できます。

10.おそろしい無用な空文でtokenを下痢

  テキスト(文字や言葉)の処理と直接の関係はありませんが、whileループを使ってtokenの取り出しなどをするとき、たまたま:
if (.....);{
  ...
  break;
}
if節のしっぽに間違って;があると、このif節の制御対象文は;(すなわち空文)になってしまい、{... break;}のほうはループの初回で無条件で実行されてしまいます。つまりループが初回でbreakしてしまいます。
  空文は正しい文ですから、エラーメッセージは出ません。しかも、コードを目視してもなかなか気づきにくい誤記です。

11. ツリーのノードのアイコン画像は簡単に指定できる

  133ページには、ツリーのノードを表す画像を変えるためにはクラスを自作する必要がある、と書いてありますが、これはウソです。あなたのプログラムのJTreeオブジェクトの変数名がmyTreeだとすると、次のような簡単なコードで好きな画像に変えられます:
DefaultTreeCellRenderer dtcr 
 = (DefaultTreeCellRenderer)(myTree.getCellRenderer()); //レンダラを取得
dtcr.setLeafIcon(new ImageIcon("my_gazou1.gif"));   //リーフノード
dtcr.setClosedIcon(new ImageIcon("my_gazou2.gif")); //サブツリー(閉じ)
dtcr.setOpenIcon(new ImageIcon("my_gazou3.gif"));   //サブツリー(開き)
  そこで、画才のあるかたは、天皇家の家系図(または家系図一般)にふさわしいアイコン画像を作って、131ページのJomeiプログラムに加えてみてください。

12. 正規表現のlookahead(前方参照, 前方先読み)には二通りの意味がある

  「Javaによるテキスト処理入門」のp272の最後のほうで:
bar\d(?=poo)
は、直後にpooのあるbar\dを指定する、と説明しています。この説明そのものは正しいのですが、lookaheadに関するもうひとつの重要な説明がなされていません。
  それは、lookaheadがパターン指定の前にある場合と後にある場合とでは意味が違う、ということです。後にある場合は、たとえば上の例です。それは文字通り、「ソレソレの直後にナニナニがある」という条件を指定します。
  しかし逆に、パターンの前にlookaheadの指定があると、「ナニナニの冒頭部分にソレソレがある」という条件を指定するのです。
  一般化して言うと:
X(?=Y) ... Yが直後にあるX
(?=Y)X ... Yの冒頭部分のX
となります。
  たとえば、
John(?= Lennon)
は、「直後に" Lennon"のある"John"」ですからJohn LennonのJohnにマッチしますが、これもまた:
(?=John Lennon)John
「現在位置(ここでは行頭)の前方に"John Lennon"があり、その"John Lennon"の冒頭部分の"John"」という指定となり、やはりJohn LennonのJohnにマッチするのです。しかしこう書くと:
(?= Lennon)John
「前方に" Lennon"があってその冒頭部分の"John"」という、ナンセンスな指定になります。


====end of contents===