HTML StripCharFilterFactoryの使い方

CharFilterの最後はHTMLStripCharFilterFactoryです。このコンポーネントのHTMLStripCharFilterは、名前の通り、入力からHTMLやXMLなどのタグを除去します。また数値参照や文字参照を、対応する文字の実体へ変換も行います。以下はwikiにも載っている、出来ることの一覧の翻訳(ほぼgoogle翻訳)です。

  • 内容を維持しながらHTML/XMLタグを削除する
    • INPUTは正確なHTML文書である必要はなく、それらしい構造だけが削除される
    • タグの中の属性も削除される。その際、属性の引用符は省略可能。
  • XML処理命令を削除する(ex: <?foo bar?> とか <?php echo "hoge"; ?>)
  • <!で始まり>で終わるXML要素を削除する
  • <script>と<style>要素の内容を削除する
    • これらの要素の内部のXMLコメントを処理(通常のコメント処理はうまく動作しないケースがある)
  • &#65;(A)または&#x7f;(DEL)のような数値文字参照を置換する(10,16進数のどちらも可)
    • &#nn;形式の10進数と&#xXX;の16進数の両方共OK
    • 終端のセミコロンはその後に空白が続く場合は省略可能
  • 全ての文字実体参照を置換する
    • &nbsp;はnon-breaking space(\u00A0)に置換する
    • 終端のセミコロンは必須(Alpha&Omega Corpのような会社名がAlphaΩ Corpに誤って置換されないように)

変換例

変換前 変換後
my <a href="www.foo.bar">link</a> my link
<br>hello<!--comment--> hello
hello<script><!-- f('<!--internal--></script>'); --></script> hello
if a<b then print a; if a<b then print a;
hello <td height=22 nowrap align="left"> hello
a<b &#65; Alpha&Omega O a<b A Alpha&Omega O
M&eacute;xico México

使い方

使い方は簡単で、設定する項目もなく、下記の例のようにTokenizerの前に設定するだけです。

<fieldType name="html_test" class="solr.TextField" positionIncrementGap="100">
  <analyzer>
    <charFilter class="solr.HTMLStripCharFilterFactory"/>
    <tokenizer class="solr.StandardTokenizerFactory"/>
  </analyzer>
</fieldType>

使用上の注意

  • 長すぎるコメントなどで削除出来ないケースがある(詳細は後述)
  • 仕様に完全に準拠している訳ではない。ブラウザの挙動にあわせている部分がある
    • CDATAの内容は無視される(XMLの文書は注意が必要)
  • storeする文書からタグが削除されるわけではなく、indexとして登録するtokenからタグが削除される(CharFilter全てに言えることですが)
    • 元の文書からタグが不要な場合は、登録前にどうにかして削除してから登録すること
    • 登録にDataImportHandlerを使っているのであれば、HTMLStripTransformerが利用可能

上記の注意点の通り、削除できないケースもあるので、「大体のケースでindexから消える」というぐらいの使い方がいいでしょう。

クラスの詳細


※Solr 3.3のものです(現行trunkではHTMLStripCharFilterはLuceneに移動しました)

処理のほとんどがHTMLStripCharFilterで行われており、Factoryはインスタンスを生成する役割のみです。これまでと同じくこれまでと同様にコードの中身を見ていくつもりだったのですが、結構大きなクラスでなのでblogで紹介していくと大変な事になります。そこで今回は、ポイントとなる処理の中心部分だけを見るだけとし、あとは主な機能を実際に使ってみて、試していこうと思います。

readメソッドの紹介

処理の起点となるreadメソッドです。そのまま載せると長すぎるので、コメント行を省いています。

public int read() throws IOException {
  if (numWhitespace > 0){
    numEaten += numWhitespace;
    addOffCorrectMap(numReturned, numEaten);
    numWhitespace = 0;
  }
  numReturned++;
  while(true) {
    int lastNumRead = numRead;
    int ch = next();
    switch (ch) {
      case '&':
        saveState();
        ch = readEntity();
        if (ch>=0) return ch;
        if (ch==MISMATCH) {
          restoreState();
          return '&';
        }
        break;

      case '<':
        saveState();
        ch = next();
        int ret = MISMATCH;
        if (ch=='!') {
          ret = readBang(false);
        } else if (ch=='/') {
          ret = readName(true);
          if (ret==MATCH) {
            ch=nextSkipWS();
            ret= ch=='>' ? MATCH : MISMATCH;
          }
        } else if (isAlpha(ch)) {
          push(ch);
          ret = readTag();
        } else if (ch=='?') {
          ret = readProcessingInstruction();
        }
        if (ret==MATCH) {
          numWhitespace = (numRead - lastNumRead) - 1;//tack on the -1 since we are returning a space right now
          return ' ';
        }
        restoreState();
        return '<';

      default: return ch;
    }
  }
}

処理の中心となる部分です。ざっくり流れを説明すると"<"もしくは"&"が見つかるまではそのままの文字を返し、見つかった場合にそれぞれの処理を行っています。また、処理の一番最初では、元の文字の位置との調整を行っているので、タグが削除されたり文字参照が解決されて文字数が変わった場合でも、元の位置は記録されています。その為、問題なくhighlighterも動作します。(管理画面で確認)

通常のコメント処理について

コメント処理のメソッドのjavadocに、HTMLの仕様に則ったコメントの例が書かれています。

/*** valid comments according to HTML specs
   <!-- Hello -->
   <!-- Hello -- -- Hello-->
   <!---->
   <!------ Hello -->
   <!>
   <!------> Hello -->

よく分からないのが一番下の例です。試しにこのクラスで試してみると「Hello -->」という結果になります。。上記のコメントの書き方だと全て消えるのが正しいと思ってしまうのですが。。なおブラウザ(chrome13, firefox5, IE8)でも同じ結果になりました。参考までにw3cHTML4の仕様を見ると、コメント内の--は避けるべきと書かれています。ちなみに、これらの例を含んだXMLを読み込むと、1つ目と3つ目以外の例はJavaXMLパーサ(Xerces)では例外となります。

SCRIPT, STYLEタグの処理について

SCRIPTとSTYLEタグは他のタグと異なり、一般的にスクリプトCSSの記述なので内容ごと削除されます。ただし、このタグの中のコメントの処理については、以下のコード内のコメントに書かれているように、少し特殊なことをやっています。

  // GRRRR.  it turns out that in the wild, a <script> can have a HTML comment
  // that contains a script that contains a quoted comment.

SCRIPTの中では以下のような意地悪なスクリプトも書くことが出来ます。

  <script><!-- document.write("comment-->") --></script>

上記のようなケースが有りうるので、文字列内のコメントは無視する必要があります。その為には読込中にタグの開閉などのHTMLの状態以外に、スクリプトの状態(クオートの開始、終了)を扱う必要があります。それにクオート内のエスケープやSSIを考慮する必要だってあります。きっとこのコメントを書いた作者はその面倒さを想像して、自然と出てきたのが「GRRRR」だったのでしょう。ちなみにちゃんと実装されており、上記の例は" "(半角スペース)が結果となります。

CDATAについて

簡単なHTMLを作って実験してみたところ、chrome13、Firefox5、IE8はCDATAの内容を表示しないようでした。このクラスでもCDATAの内容は完全に無視されます。CDATAの中に必要な文字列があるXMLを扱う場合は注意しましょう。

文字実体参照と数値文字参照の変換処理について

先に出来ること一覧にも書きましたが、数値文字参照は10進数と16進数が使えます。また空白もしくはEOFの場合はセミコロンが省略できます。文字実体参照セミコロンは省略できません。でもコード内のコメントには&ampとかの一般的なものは省略できるようにすべき、とTODOで書かれてあるので、将来的には限定的にセミコロンを省略できるようになるかもしれません。

削除できないケースについて

このFilterは処理する文字列を全てメモリ上に保持してから処理するのではなく、一文字ずつ読み込んで必要に応じて一定サイズのバッファを使って処理を行います。CharFilterには文字列の大きさは予め分からないので、全てを読み込んでOutOfMemoryErrorになるのを防ぐ理由と、元の文字列の位置を記録しておくという理由で、このような処理方法を選択しているのだと考えられます。このようにバッファを利用するという事は、前回のPatternReplaceCharFilterと同じように、バッファサイズの絡みで除去対象から漏れてしまうケースがあるという事です。バッファのサイズは8192バイトなのでそうそうオーバーすることもありませんが、困ったことにFactoryからバッファサイズを拡張する方法も今のところありません。以下に示すのが、バッファサイズオーバーで漏れてしまうケースです。

  1. 文字実体参照の置換処理
    • "&"の後の";"を見つける時に利用するバッファがオーバーするケース
  2. XML要素(<!〜〜>)の削除処理
    • "<!"の後の">"を見つける時に利用するバッファがオーバーするケース
  3. コメント要素の削除処理
    • "<!--"の後の"-->"を見つける時に利用するバッファがオーバーするケース
  4. タグの削除処理
    • "<"の後の">"を見つける時に利用するバッファがオーバーするケース
    • 属性(href="hogehoge.html"など)を読み込むときのバッファがオーバーするケース
  5. script、styleタグの内容の削除処理
    • 終了タグが見つかるまでのバッファがオーバーするケース
    • スクリプトの文字列終了('"'から次の'"'が見つかる)までのバッファがオーバーするケース
  6. XML処理命令の削除処理
    • "<?"の後の"<"を見つける時に利用するバッファがオーバーするケース

これらに該当する箇所は削除が行われず残ってしまいます。とはいっても8KBあるのでそうそうオーバーすることもありませんが、INDEXにゴミが残る可能性があります。

まとめと感想

  • タグの抽出は真面目にやると本当に大変。
  • クラスの半分近くはHTMLの文字参照の定義のコメントなのって。。
  • 怪しい実装も。。(isHexというメソッドが英数字全部trueだった)
  • 文字列を処理するコードを読むのが苦手だという事に改めて気付いた
  • コメントが人間味溢れてますw

CharFilterが終わったので次はTokenizerですが、その前にCharFilterとTokenizerとTokenFilterの役割についてまとめてみる予定です。その次は「CharFilterを作ってみた」をやります。