DOMとSAXとStAXと。

こんな名前のAPIがありますね。主にXMLの読み込みを行う為のAPI群であります。SAX以外は、書き出しもできますね。そう、SAXは書き出しできないのですね、基本的に。

<foo>
  <bar>baz</bar>
</foo>

っていうもの凄い単純なXMLを、これらのAPIでどのように扱うのか。比較なエントリ。

まずはDOM

DOMは、パース時にXMLの内容を全てメモリ上に保持し、パース後にどの要素にもいつでもアクセスできるような環境を作り出す。

import java.io.ByteArrayInputStream;
import java.io.StringWriter;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Text;

public class Dom {
  
  private static final String XML =
    "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
    +"<foo>\n"
    +"  <bar>baz</bar>\n"
    +"</foo>\n";
  
  public static void main(String[] args) throws Exception {
    Dom dom = new Dom();
    dom.read();
    dom.write();
  }

  private void read() throws Exception {
    // DOMを扱う時のオマジナイ
    DocumentBuilderFactory documentBuilderFactory
        = DocumentBuilderFactory.newInstance();
    DocumentBuilder documentBuilder
        = documentBuilderFactory.newDocumentBuilder();
    
    // StringをInputStreamに変えて、DOMパーサに食わす
    Document document = documentBuilder.parse(
        new ByteArrayInputStream(XML.getBytes("UTF-8")));
    
    // この時点で、XMLの内容は全てメモリ上に読み出されている。
    // ちなみに、XMLが想像も付かないくらいデカいと、OutOfMemoryするw
    
    // 読み込んだ情報の取り出し方(どんな順番でも読み出せるのがポイント)
    Element rootElement = document.getDocumentElement();
    System.out.println(rootElement.getNodeName());
    System.out.println(rootElement.getChildNodes().item(1).getNodeName());
    System.out.println(rootElement.getChildNodes().item(1)
        .getChildNodes().item(0).getNodeValue());
    System.out.println("0[" +rootElement.getChildNodes()
        .item(0).getNodeValue() + "]");
    System.out.println("1[" +rootElement.getChildNodes()
        .item(1).getNodeValue() + "]");
    System.out.println("2[" +rootElement.getChildNodes()
        .item(2).getNodeValue() + "]");
    
    /*
     * foo
     * bar
     * baz
     * 0[
     *   ]
     * 1[null]
     * 2[
     * ]
     */
  }
  
  private void write() throws Exception {
    // DOMを扱う時のオマジナイ
    DocumentBuilderFactory documentBuilderFactory
        = DocumentBuilderFactory.newInstance();
    DocumentBuilder documentBuilder
        = documentBuilderFactory.newDocumentBuilder();
    
    // 新しいdocumentを作る
    Document document = documentBuilder.newDocument();
    
    // documentの内容を構築する
    Element foo = document.createElement("foo");
    Element bar = document.createElement("bar");
    Text baz = document.createTextNode("baz");
    bar.appendChild(baz);
    foo.appendChild(bar);
    document.appendChild(foo);
    
    // 書き出し準備
    StringWriter stringWriter = new StringWriter();
    DOMSource source= new DOMSource(document);
    StreamResult result = new StreamResult(stringWriter);
    TransformerFactory transFactory
        = TransformerFactory.newInstance();
    Transformer transformer = transFactory.newTransformer();
    
    // 書き出し
    transformer.transform(source, result);

    System.out.println(stringWriter.toString());
    /*
     * <?xml version="1.0" encoding="UTF-8"?><foo><bar>baz</bar></foo>
     */
  }

}

普通の感覚で、オブジェクトツリー構造を作っていくイメージですね。一番直感的に扱いやすいAPIだと思います。

ただ、コメント中にも書きましたが、デメリットとしては、XMLの大きさが想定できない時には、どの位メモリを食うのか分からない事ですね。

続きましてSAX

SAXは、逐次読み込みAPIで、DOMと異なり、パース時に順番にメソッドを呼び出す。
パース終了時には、全てのXMLを読み終わった後である。パース履歴はメモリ上に保持しない為、後から過去に現れた要素にアクセスする手段は用意されていない。

import java.io.ByteArrayInputStream;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;

public class Sax {
  
  private static final String XML =
    "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
    + "<foo>\n"
    + "  <bar>baz</bar>\n"
    + "</foo>\n";
  

  public static void main(String[] args) throws Exception {
    Sax sax = new Sax();
    sax.read();
    sax.write();
  }
  
  private void read() throws Exception {
    // SAXを扱う時のオマジナイ
    SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
    SAXParser parser = saxParserFactory.newSAXParser();
    
    // StringをInputStreamに変えて、SAXパーサに食わす
    // SAXはXMLを頭から読み込み、要素やテキストなど、
    // 出現した「状況」によってハンドラのメソッドを順に呼び出す。
    parser.parse(
        new ByteArrayInputStream(XML.getBytes("UTF-8")),
        new TutorialSaxHandler()
    );
    
    // この時点で、処理は全て完了している。下記の出力結果が出力済みである。
    // XMLの内容は流れてしまい、メモリ上に残っていない。
    // 再び、要素にアクセスする手段は残っていない。
    
    /*
     * startDocument
     * startElement:foo
     * characters:[
     *   ]
     * startElement:bar
     * characters:[baz]
     * endElement:bar
     * characters:[
     * ]
     * endElement:foo
     * endDocument
     */
  }
  

  public class TutorialSaxHandler extends DefaultHandler {
    
    @Override
    public void startDocument() {
      // 開始時に一度だけ呼ばれるメソッド
      System.out.println("startDocument");
    }
    
    @Override
    public void startElement(String uri, String localName,
        String qName, Attributes attributes) {
      // 何か要素がスタートした時に呼ばれるメソッド
      // 属性も attributes で取得できる
      System.out.println("startElement:" + qName);
    }
    
    @Override
    public void characters(char[] ch, int offset, int length) {
      // 文字情報が現れた時に呼ばれるメソッド
      System.out.println("characters:[" 
          + new String(ch, offset, length) + "]");
    }
    
    @Override
    public void endElement(String uri, String localName, String qName) {
      // 何か要素が終了した時に呼ばれるメソッド
      System.out.println("endElement:" + qName);
    }
    
    @Override
    public void endDocument() {
      // 終了時に一度だけ呼ばれるメソッド
      System.out.println("endDocument");
    }
    
  }
  

  private void write() throws Exception {
    // SAXで書き出しはできません。基本的に…。
  }
  
}

このAPIであれば、どんなに大きなXMLでも、OutOfMemoryすることはありません。読んだそばから処理を行って、過去のデータは捨ててしまうAPIなので。

ただし、一度parseを始めてしまうと、処理が終わるまで呼び出し元に主導権が返って来ません。Handlerの中で拾うこともできますが、それでは色々難しい処理も出て来てしまう事がある。

そしてStAX

StAXはJava6から標準APIの仲間入りを果たしていますが、このチュートリアルではJava5環境を想定しています。ということで、ちょっくらMavenによる依存性解決をしておきます。

<dependency>
  <groupId>stax</groupId>
  <artifactId>stax-api</artifactId>
  <version>1.0.1</version>
</dependency>
<dependency>
  <groupId>stax</groupId>
  <artifactId>stax</artifactId>
  <version>1.2.0</version>
</dependency>

StAXは、下記をみてもらえれば分かりますが、SAXに似ています。逐次読み込みAPIです。しかし大きな違いは、parseの途中に好きなことができる点。InputStreamの読み出しと同様、reader.nextEvent() しない限り、StAXXMLの読み進めを行いません。

(余談ですが、StAXをラップすればSAXと同様に扱うこともできます。つまり、StAXはSAXを、より抽象化したAPIと考える事もできます。)

さらにStAXは、SAXと異なり、XMLの書き出しAPIも用意しています。

import java.io.ByteArrayInputStream;
import java.io.StringWriter;

import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

import com.bea.xml.stream.EventFactory;

public class StAX {
  
  private static final String XML =
    "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
    + "<foo>\n"
    + "  <bar>baz</bar>\n"
    + "</foo>\n";
  

  public static void main(String[] args) throws Exception {
    StAX stax = new StAX();
    stax.read();
    stax.write();
  }
  
  private void read() throws Exception {
    // StAXを扱う時のオマジナイ
    XMLInputFactory inFactory = XMLInputFactory.newInstance();
    
    // StringをInputStreamに変えて、Readerにセットする
    XMLEventReader reader = inFactory.createXMLEventReader(
        new ByteArrayInputStream(XML.getBytes("UTF-8"))
    );
    
    // この時点で、XMLの処理はまったく行われていない。準備完了しただけ。
    
    // 処理開始
    while (reader.hasNext()) {
      // XMLを一段階読み進める
      XMLEvent event = reader.nextEvent();

      // 読み込んだ内容によって振り分け
      if (event.isStartDocument()) {
        System.out.println("startDocument");
      } else if (event.isStartElement()) {
        StartElement startElement = event.asStartElement();
        System.out.println("startElement:" + startElement.getName());
      } else if (event.isEndElement()) {
        EndElement endElement = event.asEndElement();
        System.out.println("endElement:" + endElement.getName());
      } else if (event.isCharacters()) {
        Characters characters = event.asCharacters();
        System.out.println("characters:[" + characters.getData() + "]");
      } else if (event.isEndDocument()) {
        System.out.println("endDocument");
      }
    }
    // ここでXMLの読み込みが全て終了した
    reader.close();
    
    // 下記のような出力がされているハズである
    
    /*
     * startDocument
     * characters:[
     * ]
     * startElement:foo
     * characters:[
     *   ]
     * startElement:bar
     * characters:[baz]
     * endElement:bar
     * characters:[
     * ]
     * endElement:foo
     * characters:[
     * ]
     * endDocument
     */
  }
  

  private void write() throws Exception {
    // StAXを扱う時のオマジナイ
    XMLOutputFactory outFactory = XMLOutputFactory.newInstance();
    
    // 書き出し準備
    StringWriter stringWriter = new StringWriter();
    XMLEventWriter writer
        = outFactory.createXMLEventWriter(stringWriter);
    XMLEventFactory eventFactory = EventFactory.newInstance();
    
    // 実際の書き出し処理
    writer.add(eventFactory.createStartDocument());
    writer.add(eventFactory.createStartElement("", "", "foo"));
    writer.add(eventFactory.createStartElement("", "", "bar"));
    writer.add(eventFactory.createCharacters("baz"));
    writer.add(eventFactory.createEndElement("", "", "bar"));
    writer.add(eventFactory.createEndElement("", "", "foo"));
    writer.add(eventFactory.createEndDocument());
    writer.close();

    // 書き出されたものを出力してみる
    System.out.println(stringWriter.toString());
    /*
     * <?xml version='1.0' encoding='UTF-8'?><foo><bar>baz</bar></foo>
     */
  }
  
}

というわけで、要件を見極めて、これらのAPIの使い分けが色々できればいいね、というエントリでした。

# そういえば年明けの初エントリだ。遅ればせながらアケオメ。