Home日記コラム書評HintsLinks自己紹介
 

フィンローダのあっぱれご意見番 第150回「どこにあるのか分からなくても困らない」

← 前のをみる | 「フィンローダのあっぱれご意見番」一覧 | 次のをみる →

世間ではブログが割と流行っているみたいだが、 私も無料で使えるブログサービスを、いくつか使っている。 自宅で Linux のサーバーを立ち上げているのだから、 そこにブログをインストールして使うという選択もあったが、 安定性とか、回線の信頼性を考えると、 大手のブログを使った方が楽だろうと思ったのだ。 もっとも、実際に使ってみると、 本当に安定しているのか疑問を感じざるを得ない状況なのだが、 自宅のサーバーに使っていたpcが故障してしまったから、 結果的には正解だったのかもしれない。

複数のブログを使っていると、 どこに何を書いたのか分からなくなってくる。 そこで、自分のサイトだけを横断検索するような機能を用意した。 というと大げさだが、 単に namazu を使って検索するだけだ。 サイト内検索なら、Google を使うという手もあるが、 複数サイトを一度に検索させるのは結構面倒だ。

もっと大きな問題は、検索対象の箇所である。 ブログには、本文以外に、余計な情報がかなり含まれている。 例えばサイドバーに表示されているリンクだとか、 広告だとか、カレンダーとか、その類のものだ。 普段ブラウザでブログを見るときは、 これらの情報は単に便利な機能に過ぎない。 しかし、検索となると、話は別である。 これを検索対象に含めると、予期しないページがヒットしてしまうことがあるのだ。

例えば、ブログにサイト内検索の機能があって、 サイドバーに表示されているとしよう。 多分そこには「サイト内検索」と表示されている。 ということは、このプログのどのページを表示しても、 常に「検索」という言葉が含まれることになる。 言い換えれば、このブログに対して「検索」と言葉を検索すると、 本文の内容とは無関係に、 結局、このプログの全てのページがヒットしてしまうことになる。 これはあまり面白くない話だ。

では、どうすれば Google に本文だけを検索対象だと教えることができるかというと、 これまた難しい。

namazu のようなフリーの検索エンジンを使って、 自前の検索サイトを立ち上げると、 この問題は解決することができる。 ブログのページをそのままファイルに保存しないで、 本文以外を全てカットして、本文だけを保存し、 それを検索対象にすればいい。 問題は、どうやって本文だけ抜き出すかである。

§

  

実際にやってみた。 つまり、ブログのページをアクセスして、 本文だけを抜き出して、ファイルに保存する処理を書いた。 言語は Java である。 Java を使って XML を parse するには、 DOMとかSAXとか、基本的な方法があって、 説明するまでもない。 しかし、ブログのページは XML ではなく、 HTML か、XHTML で表現されている。 これを parse するには、 昔から有名なクラスだが、 javax.swing.text.html.HTML.ParserCallback というものがある。

 

※ このクラスで XHTML を parse するには多少無理がある。

このクラスを継承したクラスを作って、 必要なメソッドを override してやれば、 割と簡単に HTML を分析するプログラムを作ることができる。 例えば、List 1 にあるようなメソッドを実装してやればいい。

---- List 1 オーバーライドするメソッド ----

public void handleStartTag(HTML.Tag tag, MutableAttributeSet attributes, int position);
public void handleEndTag(HTML.Tag tag, int position);
public void handleSimpleTag(HTML.Tag tag, MutableAttributeSet attributes, int position) {
public void handleText(char[] data, int pos);

handleStartTag は、開始タグが現れた時点で呼ばれる。 例えば <p> タグが現れると、このメソッドが呼び出されるのだ。 これに対応して、</p> のような終了タグが現れると、 handleEndTag が呼び出される。

HTML では、<p>タグを閉じないような書き方も許されている。 このクラスで HTML を処理すると、 閉じるための </p> があるべき箇所に自動的に </p> が追加されて、 そこで handleEndTag が呼ばれるようにできている。 プログラムを書く側としては、 常に </p> があるものと想定してプログラムを書けるので、 割と便利だ。

§

さて、問題は、このクラスを使って <span> タグを解釈しようとしたときに起こった。 <span>タグは、タグで囲んだ間のテキストの属性を指定するような使い方をする。 だから、handleStartTag と handleEndTag が呼ばれるだろうと勝手に想像して、 そのような書き方をしたのだ。(List 2)

---- List 2 期待通りに動作しないコード ----

    public void handleStartTag(HTML.Tag tag, MutableAttributeSet attributes,
        int position) {

        if (tag == HTML.Tag.SPAN) {
            // <span> の処理をここに書く
            return;
        }
    }

    public void handleEndTag(HTML.Tag tag, int position) {
        if (tag == HTML.Tag.SPAN) {
            // </span> の処理をここに書く
            return;
        }
    }

ところが、なぜかこのメソッドが呼ばれた気配がない。 不審に思って、試しに handleSimpleTag で <span> タグを拾ってみたら、 そこで検出できたのだ。 つまり、<span> タグの処理は handleStartTag ではなく、 handleSimpleTag が呼ばれるのだ。 理由は知らないが、現にそうなっているのである。

そこでちょっとした疑問が。 </span> というタグが現れたら、 一体何が起きるのだろうか? 調べてみると、その場合は、 <span endtag="true"> というタグが現れたと解釈しているようだ。 つまり、 endtag という属性の値が true になった状態で、 handleSimpleTag が呼ばれるのである。

そこまで分かれば、プログラムを修正して 期待通り動作させるのは簡単だ。 その例が List 3 である。

---- List 3 修正して動作するようになったコード ----

    public void handleSimpleTag(HTML.Tag tag, MutableAttributeSet attributes,
        int position) {

        if (tag == HTML.Tag.SPAN) {
            handleSimpleTagSPAN(tag, attributes, position);
            return;
         }
    }

    private void handleSimpleTagSPAN(HTML.Tag tag, MutableAttributeSet attributes,
	int position) {

        String endtag = getAttributeValueString(attributes, "endtag");
        if ("true".equals(endtag)) {
            // </span> の処理をここに書く
        } else {
            // <span> の処理をここに書く
        }
    }

要するに、List 2 にあった handleStartTag と handleEndTag の処理を、 handleSimpleTag の中に移動しただけだから、 これは当然、期待通り動作する。

しかし、何かどうもひっかかる。 後で気付いたのだが、 考えてみれば、わざわざ <span> の処理を移動しなくても、 handleSimpleTag の中の処理を List 4 のように修正すればよかったのだ。

---- List 4 もう一つのやり方 ----

        if ("true".equals(endtag)) {
            handleEndTag(tag, position);
        } else {
            handleStartTag(tag, attributes, position);
        }

List 4 の場合、 <span> の処理は handleStartTag と handleEndTag で行うことになる。 最初の勘違いがそのまま生きているのである。

そこで、問題なのだが、 List 3 のような書き方と List 4 のような書き方は、 どちらが望ましいのか。 ただし、今回は効率や速度よりも可読性を重視する。 大量のテキストを parse する場合は、 確かに効率や速度も重要なのだが、 それは最後に考えればよいことで、 まだこの段階では、 いろいろ修正が入ることを想定して可読性を高める方が現実的なのだ。

ということで、 このコードを読む人が、 <span>タグの処理がどこにあると想定して読むのか、 という点に注目してみよう。 考え方は2つある。

<span>の処理は handleSimpleTag だろ、常識だ、という場合。 これはもう handleSimpleTag 内に処理がないと困る。 当たり前だが、プログラムが期待通り動くのだから、 どう書いたところで、実際に handleSimpleTag 内に処理はある。 List 4 は、さらに先に処理が分岐しているだけなのだ。 おそらく、 <span>が現れたときの処理は、それなりに長くなるわけで、 そう思えば、まとめた別処理を呼び出すというのは、 不自然な書き方ではないことになる。 ただ、それが handleStartTag だというのが小技だというのが気になる人はいるかもしれないが。

これに対して、<span> の処理を handleStartTag で行うと勘違いしている場合。 通常、<span> が現れてもその処理は呼ばれないのに、 List 4 のような書き方をしておけば、 結果的に、handleStartTag の中で処理することになる。 もしかしたら、このコードを読んだ人は、 <span> の処理は handleStartTag で書けばよい、 と勘違いしたままで終わってしまうかもしれない。 何かそれもヤバそうな話だが、 実際ヤバイのかというと、 まあ注釈にでも書いておけば、 それほどヤバいことにはならないような気もする。

折衷案として、 List 4 のような書き方をするが、 handleStartTag ではなく、handleStartTagSPAN のような名前のメソッドを追加し、 そこで処理させるような方法がある。 これなら間違いはなさそうだし、割といいような気がする。

  

(C MAGAZINE 2004年12月号掲載)
内容は雑誌に掲載されたものと異なることがあります。

修正情報:
2006-03-01 裏ページに転載。

(C) Phinloda 2004-2006, All rights reserved.