txqz memo

ひどいHTMLをSAXパーサに読ませる

先日取り上げたexblogのひどいHTMLを、HTML向けSAXパーサに読ませるとどうなるかの実験。startElement()やendElement()がどうコールされるかによって、パーサごとの性格が現れる。

コールバックメソッド

level はネストの深さを表すインスタンス変数。

public void startElement(String uri, String localName, String qName, Attributes attrs){
    for(int i = 0; i < level; i++) System.out.print(" ");
    System.out.print("<"+localName);
    for(int i = 0; i < attrs.getLength(); i++)
        System.out.print(" "+attrs.getQName(i)+"=\\""+attrs.getValue(i)+"\\"");
    System.out.println(">");
    level++;
}
public void endElement(String uri, String localName, String qName){
    level--;
    for(int i = 0; i < level; i++) System.out.print(" ");
    System.out.println("</"+localName+">");
}
public void characters(char[] ch, int offset, int length){
    for(int i = 0; i < level; i++) System.out.print(" ");
    System.out.println(StringUtils.substring(new String(ch,offset,length).trim(),0,20));
}

対象となるHTML (1)

例によって見やすいように改行とインデントを加えています。

<DIV CLASS=MNBODY>
 <TABLE WIDTH=150 BORDER=0 CELLSPACING=0 CELLPADDING=0 ALIGN=CENTER>
  <TR>
   <TD COLSPAN=3>
    <FORM NAME=finder METHOD=GET ACTION=http://www.exblog.jp/search/>
     <INPUT TYPE=HIDDEN NAME=blogid VALUE=970055211>
   </TD>
  </TR>
  <INPUT TYPE=HIDDEN NAME=t VALUE=0>
  <TR>
   <TD WIDTH=105 ALIGN=LEFT>
    <INPUT TYPE=TEXT NAME=q SIZE=15  CLASS=TXTFLD>
   </TD>
   <TD WIDTH=5></TD>
   <TD WIDTH=40 ALIGN=LEFT>
    <INPUT TYPE=SUBMIT VALUE="検索" STYLE="WIDTH:40;HEIGHT:20;">
   </TD>
  </TR>
  <TR>
   <TD COLSPAN=3>
    </FORM>
   </TD>
  </TR>
 </TABLE>
</DIV>

CyberNeko HTML Parserの場合

<DIV class="MNBODY">
 <TABLE width="150" border="0" cellspacing="0" cellpadding="0" align="CENTER">
  <TR>
   <TD colspan="3">
    <FORM name="finder" method="GET" action="http://www.exblog.jp/search/">
     <INPUT type="HIDDEN" name="blogid" value="970055211">
     </INPUT>
    <em></FORM></em>
   </TD>
  </TR>
  <INPUT type="HIDDEN" name="t" value="0">
  </INPUT>
  <TR>
   <TD width="105" align="LEFT">
    <INPUT type="TEXT" name="q" size="15" class="TXTFLD">
    </INPUT>
   </TD>

   <TD width="5">
   </TD>

   <TD width="40" align="LEFT">
    <INPUT type="SUBMIT" value="検索" style="WIDTH:40;HEIGHT:20;">
    </INPUT>
   </TD>
  </TR>
  <TR>
   <TD colspan="3">
   </TD>
  </TR>
 </TABLE>
</DIV>

td要素の終了を優先し、td要素内のform要素に終了タグを補完した。table直下にinputが残っている。

Cobra 0.98.2でも同様の結果になった。

TagSoupの場合

Tag Soupの場合、URIを指定して直接読ませたりInputStreamを読ませたりすると日本語が文字化けするので、いったんStringに落としたものをSAXParserに読ませた。

<div class="MNBODY">
 <table align="CENTER" width="150" border="0" cellspacing="0" cellpadding="0">
  <tr>
   <td colspan="3" rowspan="1">
    <form enctype="application/x-www-form-urlencoded" method="GET" name="finder" action="http://www.exblog.jp/search/">
     <input type="HIDDEN" name="blogid" value="970055211">
     </input>
     <em><input type="HIDDEN" name="t" value="0">
     </input></em>
     <tr>
      <td align="LEFT" colspan="1" rowspan="1" width="105">
       <input type="TEXT" class="TXTFLD" name="q" size="15">
       </input>
      </td>
      <td colspan="1" rowspan="1" width="5">
      </td>
      <td align="LEFT" colspan="1" rowspan="1" width="40">
       <input type="SUBMIT" value="検索" style="WIDTH:40;HEIGHT:20;">
       </input>
      </td>
     </tr>
     <tr>
      <td colspan="3" rowspan="1">
      </td>
     </tr>
    </form>
   </td>
  </tr>
 </table>
</div>

form要素の連続性を優先したが、結果としてform要素の直下にtr要素が出現してしまった。

HTMLEditorKit.ParserCallbackの場合

これはHTML3.2のパーサで、XHTMLを読ませると具合が悪い。でもついでなのでやってみた。ソースは:

public void handleStartTag(HTML.Tag tag, MutableAttributeSet attrs, int pos){
    for(int i = 0; i < level; i++) System.out.print(" ");
    System.out.print("<"+tag);
    Enumeration e = attrs.getAttributeNames();
    while(e.hasMoreElements()){
        Object o = e.nextElement();
        System.out.print(" "+o+"=\\""+attrs.getAttribute(o)+"\\"");
    }
    System.out.println(">");
    level++;
}
public void handleSimpleTag(HTML.Tag tag, MutableAttributeSet attrs, int pos){
    for(int i = 0; i < level; i++) System.out.print(" ");
    System.out.print("<"+tag);
    Enumeration e = attrs.getAttributeNames();
    while(e.hasMoreElements()){
        Object o = e.nextElement();
        System.out.print(" "+o+"=\\""+attrs.getAttribute(o)+"\\"");
    }
    System.out.println(" />");
}
public void handleEndTag(HTML.Tag tag, int pos){
    level--;
    for(int i = 0; i < level; i++) System.out.print(" ");
    System.out.println("</"+tag+">");
}
public void handleText(char[] data, int pos){
    for(int i = 0; i < level; i++) System.out.print(" ");
    System.out.println(StringUtils.substring(new String(data).trim(),0,20));
}

結果は

<div class="mnbody">
 <table cellspacing="0" align="center" border="0" width="150" cellpadding="0">
  <tr>
   <td colspan="3">
    <form name="finder" action="http://www.exblog.jp/search/" method="get">
     <input value="970055211" name="blogid" type="hidden" />
    </form>
   </td>
  </tr>
  <em><tr _implied_="true">
   <td _implied_="true"></em>
    <input value="0" name="t" type="hidden" />
   </td>
  </tr>
  <tr>
   <td align="left" width="105">
    <input name="q" size="15" type="text" class="txtfld" />
   </td>
   <td width="5">
   </td>
   <td align="left" width="40">
    <input style="WIDTH:40;HEIGHT:20;" value="検索" type="submit" />
   </td>
  </tr>
  <tr>
   <td colspan="3">
   </td>
  </tr>
 </table>
</div>

table直下のinput要素をtrとtdでラップした。3つの中では結構成功に近い結果を出したのではないだろうか。ただ、このパーサはXHTMLにまったく対応していないのが残念。バージョンアップしないのかな。

Mozilla Java Html Parserの場合

これはSAXパーサではないので、DOMをプリントする必要があるが、その結果以下のようになった:

<div class="MNBODY">
 <table width="150" border="0" cellspacing="0" cellpadding="0" align="CENTER">
  <tbody>
   <tr>
    <td colspan="3">
     <form name="finder" method="GET" action="http://www.exblog.jp/search/">
      <input type="HIDDEN" name="blogid" value="970055211"></input>
     </form>
    </td>
    <input type="HIDDEN" name="t" value="0"></input>
    <tr>
     <td width="105" align="LEFT">
      <input type="TEXT" name="q" size="15" class="TXTFLD"></input>
     </td>
     <td width="5"></td>
     <td width="40" align="LEFT">
      <input type="SUBMIT" value="検索" style="WIDTH:40;HEIGHT:20;"></input>
     </td>
    </tr>
    <tr>
     <td colspan="3">
     </td>
    </tr>
   </tr>
  </tbody>
 </table>
</div>

tbodyが補完されているのがいかにもMozillaのパーサという感じだが、tr直下にinputやtrが来てしまっている点が惜しい。とはいえ、これ以上どう修正すればいいのだろう。

HTMLCleanerの場合

<div class="MNBODY">
 <input name="t" type="HIDDEN" value="0"></input>
 <table align="CENTER" border="0" cellpadding="0" cellspacing="0" width="150">
  <tbody>
   <tr>
    <td colspan="3">
     <form action="http://www.exblog.jp/search/" method="GET" name="finder">
      <input name="blogid" type="HIDDEN" value="970055211"></input>
     </form>
    </td>
   </tr>
   <tr>
    <td align="LEFT" width="105">
     <input class="TXTFLD" name="q" size="15" type="TEXT"></input>
    </td>
    <td width="5"></td>
    <td align="LEFT" width="40">
     <input style="WIDTH:40;HEIGHT:20;" type="SUBMIT" value="検索"></input>
    </td>
   </tr>
   <tr>
    <td colspan="3"></td>
   </tr>
  </tbody>
 </table>
</div>

実はMozillaParserより頭が良かったりして。

対象となるHTML (2)

もう1つ例を挙げる。p要素下にdiv要素があり、さらにその子供にp要素があるケース。具体的に言うと自民大敗、本当の理由 (ニュースを斬る):NBonline(日経ビジネス オンライン)。("(中略)"以外のコメントは元からあったもの)

<td valign="top" rowspan="2" class="main_part">
 <div id="articlecontent">
  <h1 class="articleheader">自民大敗、本当の理由</h1>
  <h2 class="articleheader">小泉改革路線に逆行する安倍自民を国民が拒絶</h2>
  <ul class="articledata">
   <li>2007年8月7日 火曜日</li>
   <li><a href="/bns/author.jsp?ID=131744&amp;OFFSET=0">谷川 博</a></li>
  </ul>
  <div class="articlekeyword_wrapper">
   <div class="articlekeyword">
    <a href="/bns/bnsearch.jsp?BID=1006&amp;OFFSET=0&amp;SEARCH_TEXT=%CF%C0%C5%C0">論点</a> 
    <a href="/bns/bnsearch.jsp?BID=1006&amp;OFFSET=0&amp;SEARCH_TEXT=%BB%B2%B1%A1%C1%AA">参院選</a> 
    <a href="/bns/bnsearch.jsp?BID=1006&amp;OFFSET=0&amp;SEARCH_TEXT=%B0%C2%C7%DC%C0%AF%B8%A2">安倍政権</a> 
    <a href="/bns/bnsearch.jsp?BID=1006&amp;OFFSET=0&amp;SEARCH_TEXT=%BE%AE%C0%F4%B2%FE%B3%D7">小泉改革</a> 
   </div>
  </div>
  <p>
   <!--囲み -->
   <div class="kakomi">
    <p>参院選での自民党大敗については、既に年金問題、政治とカネ、閣僚の失言などいくつもの原因が挙げられている。だが、河野太郎・衆議院議員は根底にある本質的な敗因は、小泉改革の継承者であるはずの安倍政権と自民党が継承者としての責務を果たさず、むしろ逆行していることだと断ずる。「昔の自民党」に戻るなら、次の選挙も危うい。(聞き手は、日経ビジネス オンライン=谷川 博)</p>
   </div>
   <!--//囲み//  -->
  </p>
  <p>
   <!--画像 -->
   <div class="figure" style="width:250px;">
    <img src="250px1.jpg">
    <p>河野太郎・衆議院議員</p>
   </div>
   <!--//画像// -->
  </p>
  <p><strong>NBO</strong> 参院選は自民党の「歴史的大敗」となりました。年金の記録漏れ問題や「政治とカネ」の問題、閣僚の失言など様々な問題が重なり、自民党に激しい逆風が吹いた結果だと言われています。</p>
  <em><!--(中略)--></em>
  <div class="blocktitle_l">
   <iframe width="528" height="100" marginwidth="0" marginheight="0" hspace="0" vspace="0" frameborder="0" scrolling="no" bordercolor="#000000" src="http://bizad.nikkeibp.co.jp/NBP_AD/nbonline/ad/nbo_thisweek.shtml">
   </iframe>
  </div>
 </div>
</td>

インタビュー本文のある段落をCSSセレクタ風に書くと、 td.main_part > div#articlecontent > p となる。

CyberNeko HTML Parserの場合

長くなるので、一部改行とインデントを削除した。

<TD valign="top" rowspan="2" class="main_part">
 <DIV id="articlecontent">
  <H1 class="articleheader">自民大敗、本当の理由</H1>
  <H2 class="articleheader">
   小泉改革路線に逆行する安倍自民を国民が拒
  </H2>
  <UL class="articledata">
   <LI>2007年8月7日 火曜日</LI>
   <LI><A href="/bns/author.jsp?ID=131744&amp;OFFSET=0">谷川 博</A></LI>
  </UL>
  <DIV class="articlekeyword_wrapper">
   <DIV class="articlekeyword">
    <A href="/bns/bnsearch.jsp?BID=1006&amp;OFFSET=0&amp;SEARCH_TEXT=%CF%C0%C5%C0">論点</A> 
    <A href="/bns/bnsearch.jsp?BID=1006&amp;OFFSET=0&amp;SEARCH_TEXT=%BB%B2%B1%A1%C1%AA">参院選</A> 
    <A href="/bns/bnsearch.jsp?BID=1006&amp;OFFSET=0&amp;SEARCH_TEXT=%B0%C2%C7%DC%C0%AF%B8%A2">安倍政権</A> 
    <A href="/bns/bnsearch.jsp?BID=1006&amp;OFFSET=0&amp;SEARCH_TEXT=%BE%AE%C0%F4%B2%FE%B3%D7">小泉改革</A> 
   </DIV>
  </DIV>
  <em><P>
   <DIV class="kakomi">
   </DIV>
  </P>
  <P>
   参院選での自民党大敗については、既に年金
   の選挙も危うい。(聞き手は、日経ビジネス
  </P></em>
 </DIV>
 <P>
 </P>
 <P>
  <DIV class="figure" style="width:250px;">
   <IMG src="250px1.jpg">
   </IMG>
  </DIV>
 </P>
 <P>河野太郎・衆議院議員</P>
 <P>
 </P>
 <P>
  <STRONG>NBO</STRONG> 参院選は自民党の「歴史的大敗」となりま
 </P>
 <!--中略-->
 <DIV class="blocktitle_l">
  <IFRAME width="528" height="100" marginwidth="0" marginheight="0" hspace="0" vspace="0" frameborder="0" scrolling="no" bordercolor="#000000" src="http://bizad.nikkeibp.co.jp/NBP_AD/nbonline/ad/nbo_thisweek.shtml">
  </IFRAME>
 </DIV>
</TD>

「参院選での自民党大敗」~の段落が div.kakomi から外に出てしまっている。そのあおりで、インタビュー本文のある段落の場所も td.main_part > p となってしまった。なので、NekoHTMLのDOMパーサで div.kakomidiv#articlecontent を取得しようと思ってもうまくいかない。前者は空白のみで、後者は「自民大敗、本当の理由」から」(聞き手は、日経ビジネス オンライン=谷川 博)」までしか取得できない。

TagSoupの場合

上と同じように、一部改行と空白を削除したところがある。

<td colspan="1" rowspan="2" valign="top" class="main_part">
 <div id="articlecontent">
  <h1 class="articleheader">自民大敗、本当の理由</h1>
  <h2 class="articleheader">
    小泉改革路線に逆行する安倍自民を国民が拒</h2>
  <ul class="articledata">
   <li>2007年8月7日 火曜日</li>
   <li>
    <a shape="rect" href="/bns/author.jsp?ID=131744&amp;OFFSET=0">谷川 博</a>
   </li>
  </ul>
  <div class="articlekeyword_wrapper">
   <div class="articlekeyword">
    <a shape="rect" href="/bns/bnsearch.jsp?BID=1006&amp;OFFSET=0&amp;SEARCH_TEXT=%CF%C0%C5%C0">論点</a> 
    <a shape="rect" href="/bns/bnsearch.jsp?BID=1006&amp;OFFSET=0&amp;SEARCH_TEXT=%BB%B2%B1%A1%C1%AA">参院選</a> 
    <a shape="rect" href="/bns/bnsearch.jsp?BID=1006&amp;OFFSET=0&amp;SEARCH_TEXT=%B0%C2%C7%DC%C0%AF%B8%A2">安倍政権</a> 
    <a shape="rect" href="/bns/bnsearch.jsp?BID=1006&amp;OFFSET=0&amp;SEARCH_TEXT=%BE%AE%C0%F4%B2%FE%B3%D7">小泉改革</a> 
   </div>
  </div>
  <em><p></p>
  <div class="kakomi">
   <p>
    参院選での自民党大敗については、既に年金
   </p>
  </div>
  <p></p></em>
  <div class="figure" style="width:250px;">
   <img src="250px1.jpg"></img>
   <p>河野太郎・衆議院議員</p>
  </div>
  <p>
   <strong>NBO</strong> 参院選は自民党の「歴史的大敗」となりま
  </p>
  <!--中略-->
  <div class="blocktitle_l">
   <iframe frameborder="0" scrolling="no" width="528" height="100" marginwidth="0" marginheight="0" hspace="0" vspace="0" bordercolor="#000000" src="http://bizad.nikkeibp.co.jp/NBP_AD/nbonline/ad/nbo_thisweek.shtml">
   </iframe>
  </div>
 </div>
</td>

こちらは、p要素下にdiv要素が来ることを防いだ。 div#articlecontent がちゃんと直親td要素の終了直前まで続いているので、 //div[@id='articlecontent'] というXPATH式で本文部分全体を取得できる。

HTMLEditorKit.ParserCallbackの場合

このパーサは想定外の要素に弱いみたいで、処理中に ArrayIndexOutOfBoundsException が発生した。原因は84行目をはじめ随所に現れるcomment要素で、これらを削除したところ例外を出さずに処理を終えることができ、TagSoupと似た結果になった。

これら結果を見る限り、HTMLの文法を最も理解しているのはHTMLEditorKit.ParserCallbackのように思える。しかし、このパーサはSAX標準のインタフェースに則っておらず、イリーガルな要素が現れると例外が出てしまうというのが痛い。HTML4.01/XHTML1.0に準じるようなバージョンアップをしてくれないかなー。TagSoupは評判どおり中々優秀だが、InputStreamの日本語を正しく理解してくれないのが難点。日本語の問題を解決したカスタム版があるっぽい? でもこれは実体参照されてしまう問題を解決するためのものだから違うか。1回Stringにすれば問題ないので使えないわけではない。

ShaniXmlParserの場合

ShaniXMLParserは、HTMLのエラー訂正をDOM層でやっているぽい (DocumentBuilderFactoryでDTDを読むかどうかの指定ができるが、SAXParserFactoryにはない) のでちょっと特殊 (<code class="html"><img ~ ></code> となっていると endElement() がコールされない。 <code class="html"><img ~ /></code> だとコールされる) だ。

Mozilla Java Html Parserの場合

空のp要素ノードが2つ多く生成されたことを除けば、TagSoupの出力結果と同じになった。