txqz memo

OperaのXMLパーサってすごかったのね

OperaにXMLファイルを読ませるとベタテキストが出てくる。フィードだと「新規購読」というダイアログが出るけれども、画面に表示されるのは容赦ないベタテキストだ。IEやFirefoxにスタイル情報のないXMLを渡せばドキュメントツリーを表示してくれるのと対照的で、どうもOperaはXMLにやる気がないのかと勝手に思っていた。

だが、整形式でないXMLを投げた場合、Operaのエラー表示が圧倒的に見やすいことに最近気づいた。たとえば、以下のようなXMLをブラウザに表示させてみる:

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <hoge>あああ</hoge>
    <hoge>いいい
    <hoge>ううう</hoge>
</root>

Firefoxだと:

XML パースエラー: タグの対応が間違っています。終了タグが必要です: </hoge>
URL: file:///D:/test.xml
行番号: 6, 列番号: 3:

</root>
--^

IEだと:

XML ページを表示できません 
XSL スタイル シートを使用した XML 入力は表示できません。エラーを訂正してください。 [更新] ボタンをクリックするか、または後でやり直してください。 


--------------------------------------------------------------------------------

終了タグ 'root' が開始タグ 'hoge' と一致していません。リソース 'file:///D:/test.xml' の実行エラーです。ライン 6、位置 3

</root>
--^

どちらも、6行目に問題があると言ってくる。だが、では </hoge> を追加しようとしても、いったい何行目のhoge要素が閉じられていないのか分からないのだ。でもOperaは違う。

Operaの場合:

エラー!
XML の解析に失敗しました

XML の解析に失敗しました: 構文エラー (行: 6, 文字: 0)

HTML ドキュメントとして再解析する
エラーmismatched end-tag
仕様http://www.w3.org/TR/REC-xml/
  1: <?xml version="1.0" encoding="UTF-8"?>
  2: <root>
  3:     <hoge>あああ</hoge>
  4:     <em><hoge></em>いいい
  5:     <hoge>ううう</hoge>
  6: <em></root></em>

と、何行目のhoge要素のせいで6行目のエラーが発生したのかを強調表示で教えてくれる。これは6行しかないから目視で分かるけど、インデントなしで千何行もあったら人間の仕事じゃなくなるわけで、Operaのこの挙動は大変ありがたい。

Xercesも

[Fatal Error] :6:3: The element type "hoge" must be terminated by the matching end-tag "</hoge>".

にとどまる。何行目のせいで整形式になっていないのかをちゃんと教えてくれるパーサってOperaくらいなのかなー。

ちなみに、不正なXMLにも対応しているパーサでこういうXMLを読ませると、以下のようになる。

CyberNeko HTML Parserの場合は (以下mainメソッドが例外をスローしてたりXMLをStringから読んでいたりしてやる気のないソースなので注意):

package net.txqz.test.xml;

import java.io.IOException;
import java.io.StringReader;

import org.cyberneko.html.parsers.DOMParser;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

public class NekoTest {
    public static void main(String[] args) throws SAXException, IOException {
        String xml = "<?xml version='1.0' encoding='utf-8'?>\n<root>\n<hoge>あああ</hoge>\n<hoge>いいい\n<hoge>ううう</hoge>\n</root>";
        DOMParser parser = new DOMParser();
        parser.parse(new InputSource(new StringReader(xml)));
        Document document = parser.getDocument();
        NodeList nodes = document.getElementsByTagName("hoge");
        for(int i = 0; i < nodes.getLength(); i++){
            Node node = nodes.item(i);
            System.out.println(node.getNodeName()+":"+node.getTextContent());
        }
    }
}

出力結果は:

HOGE:あああ
HOGE:いいい
ううう

HOGE:ううう

ShaniXmlParserの場合は:

package net.txqz.test.xml;

import java.io.IOException;
import java.io.StringReader;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

public class ShaniTest {
    public static void main(String[] args) throws SAXException, IOException, ParserConfigurationException{
        String xml = "<?xml version='1.0' encoding='utf-8'?>\n<root>\n<hoge>あああ</hoge>\n<hoge>いいい\n<hoge>ううう</hoge>\n</root>";
        System.setProperty("javax.xml.parsers.DocumentBuilderFactory","org.allcolor.xml.parser.CDocumentBuilderFactory");
        System.setProperty("javax.xml.parsers.SAXParserFactory","org.allcolor.xml.parser.CSaxParserFactory");
        DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();

        Document document = builder.parse(new InputSource(new StringReader(xml)));
        NodeList nodes = document.getElementsByTagName("hoge");
        for(int i = 0; i < nodes.getLength(); i++){
            Node node = nodes.item(i);
            System.out.println(node.getNodeName()+":"+node.getTextContent());
        }
    }
}

出力結果は:

hoge:あああ
hoge:いいい
ううう
hoge:ううう

Jericho HTML Parserの場合は:

package net.txqz.test.xml;

import java.io.IOException;
import java.io.StringReader;
import java.util.List;

import au.id.jericho.lib.html.Element;
import au.id.jericho.lib.html.Source;

public class JerichoTest {
    public static void main(String[] args) throws IOException {
        String xml = "<?xml version='1.0' encoding='utf-8'?>\n<root>\n<hoge>あああ</hoge>\n<hoge>いいい\n<hoge>ううう</hoge>\n</root>";
        Source src = new Source(new StringReader(xml));
        List<Element> list = src.findAllElements("hoge");
        for(Element element : list){
            System.out.println(element.getName()+":"+element.extractText());
        }
    }
}

出力結果は:

<pre><samp>hoge:あああ
hoge:
hoge:ううう

Jericho HTML Parserは w3c DOMを使わずに独自の形式でHTMLやXMLをパースするのだけれども、不正なXMLもパースできるのがウリなのにこの方法で「いいい」を抽出できなかった。やりかたが悪い? いやまぁ整形式でないのが一番悪いのですが。ほかのパーサも「いいい」だけ抽出するのは不可能で、「いいい」と「ううう」がセットになる。それは、パーサがこの適当なXMLのDTDを知らないから当然の処理。HTMLで

<ul><li>あああ<li>いいい<li>ううう</ul>

とかなっている場合は、HTMLパーサと名乗っている以上li要素の暗黙の終了タグをちゃんと補完してくれることでしょう。

あと、いま知ったのだけれども、XML宣言ってversionを必ず最初に指定しないといけないのね。encodingを先に指定したら[Fatal Error] :1:23: The version is required in the XML declaration.とXercesに怒られた。Xerces厳しすぎワロタと思ったら仕様には

[23] XMLDecl ::= '<?xml' VersionInfo EncodingDecl? SDDecl? S? '?>'

って書いてあった。ふつうXMLの属性は登場順序に意味がないので、宣言もそうだと思い込んでいた。いまさらー。


XMLはツリー表示されることを規定しないのでOperaはCSS初期値で表示してる(cf. http://kuruman.org/old_diary/200608#D19-01 )。パースエラーの開始箇所候補を挙げてくれるのはありがたいよね。

なるほど。やっぱりはOperaすばらしいですね。