第 8 回 DOM

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

8-1. DOM

DOM は XML 文書などをプログラムで扱うために提供される、オブジェクト指 向型の API です。 もともとは、 Netscape 社が提案した Dynamic HTML という技術が発端です。 これはブラウザが読み込んだ HTML を Javascript 言語が取り扱えるようにす るために、HTML 文書に API を与えたものです。 すぐに Microsoft 社が DHTML と JScript というよく似ているけど、互換性 があまりない形態で追従しました。 これらは現在では DOM Level 0 と呼ばれていますが、これは規格化されたも のではありません。

W3C が制定している DOM は Level 1, 2, 3 があります。 これらはプログラミング言語には中立ということで、 OMG IDL(ISO14750) というインターフェイス記述言語により定義されています。 但し、規格書の中では Java と Javascript(ECMA Script) の両方の記述も含 まれています。

現在の主なブラウザは DOM Level 2 まで準拠していると言えます。 また、 Java 6 からは、 DOM Level 3 Core に対応しています。

DOM Level 3 の基本 API

DOM Level 3 Core は XML 用のインタフェイスを記述しています。 これから説明するのは、 org.w3c.dom パッケージの内容です。 この節では主要なインタフェイスについて説明します。

Node

Node は DOM のすべてのオブジェクトが実装しているインタフェイスです。

まず、Node は名前と、値を持ちます。 値に関しては setter も用意されています。


package org.w3c.dom;
public interface Node {
    public String getNodeName();
    public String getNodeValue() throws DOMException;
    public void setNodeValue(String nodeValue) throws DOMException;
...

ノードの種類によって、番号が割り当てられていて、 getNodeType で 番号を取り出せます。


    public short getNodeType();
    public static final short ELEMENT_NODE              = 1;
    public static final short ATTRIBUTE_NODE            = 2;
    public static final short TEXT_NODE                 = 3;
    public static final short CDATA_SECTION_NODE        = 4;
    public static final short ENTITY_REFERENCE_NODE     = 5;
    public static final short ENTITY_NODE               = 6;
    public static final short PROCESSING_INSTRUCTION_NODE = 7;
    public static final short COMMENT_NODE              = 8;
    public static final short DOCUMENT_NODE             = 9;
    public static final short DOCUMENT_TYPE_NODE        = 10;
    public static final short DOCUMENT_FRAGMENT_NODE    = 11;
    public static final short NOTATION_NODE             = 12;

また 子ノードを持つので、それに関するメソッドが定義されています。


    public Node getParentNode();
    public NodeList getChildNodes();
    public Node getFirstChild();
    public Node getLastChild();
    public Node getPreviousSibling();
    public Node getNextSibling();
    public Node insertBefore(Node newChild, Node refChild) throws DOMException;
    public Node replaceChild(Node newChild, Node oldChild) throws DOMException;
    public Node removeChild(Node oldChild) throws DOMException;
    public Node appendChild(Node newChild) throws DOMException;
    public boolean hasChildNodes();

また、 子ノードの他に、Attribute に関するメソッドもあります。


    public boolean hasAttributes();
    public NamedNodeMap getAttributes();

なお、上記のメソッドで示されていますが、子ノードは NodeList 形式で集め られ、 Attibute は NamedNodeMap という形式で集められています。

NodeList

NodeList の定義は以下の通りです。これは抜粋ではなく、すべてです。


package org.w3c.dom;
public interface NodeList {
    public Node item(int index);
    public int getLength();
}

このように、 NodeList 型は単に番号により Node が取り出せるだけの仕組み しか持ってなく、 iterator や for each 構文などもそのままでは使えません。

NamedNodeMap

NamedNodeMap の定義は以下がすべてです。


package org.w3c.dom;
public interface NamedNodeMap {
    public Node item(int index);
    public int getLength();

    public Node getNamedItem(String name);
    public Node setNamedItem(Node arg) throws DOMException;
    public Node removeNamedItem(String name) throws DOMException;

    public Node getNamedItemNS(String namespaceURI, String localName);
    public Node setNamedItemNS(Node arg) throws DOMException;
    public Node removeNamedItemNS(String namespaceURI, String localName)
                                  throws DOMException;
}

NodeList と同様に番号で呼び出すメソッドもありますが、 getNamedItem と いうメソッドで名前から Node を取り出すことができます。 setNamedItem では Node の名前を使って登録します。 これが void 型でないのは、置き換えられる Node がある場合に、それが返る ようになっているからです。 また、名前空間を指定するメソッドもあります。

XML に関するインタフェース

これから、Node を継承した各データタイプの定義を見ていきます。 各データタイプにおいて、 nodeName, nodeValue, attributes の解釈が変わっ てきます。 始めに、 org.w3c.dom.Node のマニュアルにに記載されている表を抜粋します。

インタフェース nodeName nodeValue attributes
Attr Attr.name と同じ Attr.value と同じ null
CDATASection "#cdata-section" CharacterData.data (CDATA セクションの内容) と同じ null
Comment "#comment" CharacterData.data (コメントの内容) と同じ null
Document "#document" null null
DocumentFragment "#document-fragment" null null
DocumentType DocumentType.name と同じ null null
Element Element.tagName と同じ null NamedNodeMap
Entity エンティティー名 null null
EntityReference 参照されるエンティティーの名前 null null
Notation 表記法名 null null
ProcessingInstruction ProcessingInstruction.target と同じ ProcessingInstruction.data と同じ null
Text "#text" CharacterData.data (テキストノードの内容) と同じ null

Element

nodeType = 1 である、 ELEMENT_NODE のクラスは、値、名前、属性すべてを 持っています。 なお、 getNodeName と getTagName は同じ値になります。 また、 getAttribute(文字列) は、 getAttributes().getNamedItem(文字列) と同じです。 なお、setAttribute(文字列, 文字列) は自動的に属性オブジェクトを生成し て要素を追加してくれるので便利ですが、CDATA の検査などはしないとマニュ アルに注釈があります。

Attr

属性を示すノードは org.w3c.dom.Attr インタフェースです。 Element で説明しましたが、Element では文字列を介して属性値を操作可能に するメソッドが追加されてました。 そのため、このノードを使用しなくても属性値の操作は可能です。

Text, Comment, CDATASection

Text, Comment, CDATASection は Node を直接継承せず、 CharacterData イ ンタフェースを継承しています。 CharacterData インタフェースは下記のように、文字列に関するメソッドを追 加しています。 なお、CharacterData インタフェースの使用する文字は UTF-16 で記述されていると 仮定されています。 そのため、文字列操作においては 16 bit が基準になっています。


package org.w3c.dom;
public interface CharacterData extends Node {
    public String getData() throws DOMException;
    public void setData(String data) throws DOMException;
    public int getLength();
    public String substringData(int offset, int count) throws DOMException;
    public void appendData(String arg) throws DOMException;
    public void insertData(int offset, String arg) throws DOMException;
    public void deleteData(int offset, int count) throws DOMException;
    public void replaceData(int offset, int count, String arg) throws DOMException;
}

この CharacterData インタフェースをそれぞれ継承するのが Text, Comment, CDATASection ですが、 Text に splitText メソッドが追加される他は、メソッ ドは追加されません。


package org.w3c.dom;
public interface Text extends CharacterData {
    public Text splitText(int offset) throws DOMException;
}

package org.w3c.dom;
public interface Comment extends CharacterData {
}

package org.w3c.dom;
public interface CDATASection extends Text {
}

Document

Document は DOM の根の Node になります。 そのため、管理的なメソッドが定義されています。 Doctype, 根の Element が取得できます。


public interface Document extends Node {
    public DocumentType getDoctype();
    public Element getDocumentElement();
...

また、文書型定義にしたがって要素や属性などのファクトリが定義されていま す。 これを用いて様々な Node を作り、加えるなど操作を行います。


    public Element createElement(String tagName) throws DOMException;
    public DocumentFragment createDocumentFragment();
    public Text createTextNode(String data);
    public Comment createComment(String data);
    public CDATASection createCDATASection(String data) throws DOMException;
    public ProcessingInstruction createProcessingInstruction(String target,
                                           String data) throws DOMException;
    public Attr createAttribute(String name) throws DOMException;
    public EntityReference createEntityReference(String name) throws DOMException;

さらに、 XML 文書から特定のタグや ID を用いて要素を抜き出すメソッドも あります。


    public NodeList getElementsByTagName(String tagname);
    public Element getElementById(String elementId);

この他、名前空間を指定するものなどもあります。

8-2. DOM の用法

ファイルとのやりとり

XML 文書を DOM に変換するには、javax.xml.parsers パッケージを使います。 ここにある、DocumentBuilder クラスに実装されている、 parse メソッドは、 java.io.File オブジェクト、または java.io.InputStream オブジェクトを受 け取り、 org.w3c.dom.Document オブジェクトを返します。 但し、 DocumentBuilder には public なコンストラクタがありませんので、 ファクトリクラスである、 DocumentBuilderFactory を使用する必要がありま す。 これにも public なコンストラクタは無く、 newInstance() とい う static メソッドを呼んでオブジェクトを作った後、 newDocumentBuilder メソッドを呼ぶ必要があります。 つまり、次のようにします。


  File f = new File(ファイル名);
  DocumentBuilderFactory dbf
                        = DocumentBuilderFactory.newInstance();
  DocumentBuilder db = dbf.newDocumentBuilder();
  Document doc = db.parse(f);

一方、 DOM オブジェクトを XML 文書にするには、 java.xml.transform.Transformer を使用します。 これは、汎用の変換オブジェクトで、 transform メソッドを使って特定の型 から特定の型へと変換します。 transform メソッドの引数は javax.xml.transform.Source 型と、 javax.xml.transform.Result 型です。 Source 型には XML を入力しますが、 DOM を入力するには javax.xml.transform.dom.DOMSource 型のオブジェクトを与えます。 これはコンストラクタに Node のインスタン スを与えて作成します。 一方、 Result 型ですが、標準出力などの Stream にするには、 javax.xml.transform.stream.StreamResult のオブジェクトに与えます。 これも同様にコンストラクタに File, OutputStream, Writer のオブジェクトを与えます。 なお、文字コードを指定する場合は、 java.util.Properties オブジェクトに encoding として文字コードを与え、 Transform オブジェクトの setOutputProperties メソッドに与えます。 なお、この setOutputProperties メソッドで与えられる Property に許され ている属性値は javax.xml.transform.OutputKeys に定義されています。 indent 属性を yes にすると、表示が整形されます。

例8-1

標準入力から入力した XML 文書を DOM に変換した後、DOM を標準出力に出力 するプログラムです。 実行するには java Rei < サンプル.xml と標準入力から XML 文書を与えます。


import java.io.*;
import java.util.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import org.w3c.dom.*;
class Rei {
    private static Document getDocument(InputStream is) throws Exception {
	DocumentBuilderFactory dbf
	    = DocumentBuilderFactory.newInstance();
	DocumentBuilder db = dbf.newDocumentBuilder();
	return db.parse(is);
    }
    private static void printDocument(Document doc) throws Exception {
	TransformerFactory tfactory = TransformerFactory.newInstance(); 
	Transformer transformer = tfactory.newTransformer(); 
	Properties prop = new Properties();
	prop.setProperty("encoding", "Shift_JIS");
	prop.setProperty("indent", "yes");
	transformer.setOutputProperties(prop);
	transformer.transform(new DOMSource(doc),
            new StreamResult(System.out));
    }
    public static void main(String[] args) throws Exception {
	Document doc = getDocument(System.in);
        printDocument(doc);	
    }
}

サンプル XML

<?xml version="1.0" encoding="shift_jis" ?>
<class name="データ構造とアルゴリズム II" >
<teacher name="坂本直志" />
<time day="tue" period="2" />
</class>

DOM の作成

次に、 DOM をメソッドで作成し、 XML 文書として出力する例を示します。 DocumentBuilder でファイルを読まず、 newDocument メソッドで新しい まず、Document オブジェクトを作成します。 その後、この Document オブジェクトを Node のファクトリとして使用し、作っ た Node を appendChild メソッドで次々に加えていきます。 また、 Element に対して属性を与えるため、 setAttribute を使います。

例8-2


import java.io.*;
import java.util.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import org.w3c.dom.*;
class Rei {
    private static Document getDocument() throws Exception {
	DocumentBuilderFactory dbf
	    = DocumentBuilderFactory.newInstance();
	DocumentBuilder db = dbf.newDocumentBuilder();
	return db.newDocument();
    }
    private static void printDocument(Document doc) throws Exception {
	TransformerFactory tfactory = TransformerFactory.newInstance(); 
	Transformer transformer = tfactory.newTransformer(); 
	Properties prop = new Properties();
	prop.setProperty("encoding", "Shift_JIS");
	transformer.setOutputProperties(prop);
	transformer.transform(new DOMSource(doc),
            new StreamResult(System.out));
    }
    public static void main(String[] args) throws Exception {
	Document doc = getDocument();

	Element root = doc.createElement("class");
	root.setAttribute("name","データ構造とアルゴリズム II");
	doc.appendChild(root);
	Element teacher = doc.createElement("teacher");
	teacher.setAttribute("name","坂本直志");
	root.appendChild(teacher);
	Element time = doc.createElement("time");
	time.setAttribute("day","tue");
	time.setAttribute("period","2");
	root.appendChild(time);
	
        printDocument(doc);
    }
}

DOM の変形

次に、読み込んだ XML 文書に対して、すべての Element に ID をつけること を考えます。 ID 属性は文書中で唯一でなければなりません。 そのため、もともと存在している ID に対しては重複しないようにします。 このためには、一旦すべて読み込んでから、重複のチェックができるようにし ます。 これを行うために、getElementsByTagName メソッドを使用します。 これはワイルドカード * が使用できるので、これを使うことで、すべての Element にアクセスできます。


import java.io.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import org.w3c.dom.*;
import java.util.*;
class Rei {
    private static Document getDocument(InputStream is) throws Exception {
	DocumentBuilderFactory dbf
	    = DocumentBuilderFactory.newInstance();
	DocumentBuilder db = dbf.newDocumentBuilder();
	return db.parse(is);
    }
    private static void printDocument(Document doc) throws Exception {
	TransformerFactory tfactory = TransformerFactory.newInstance(); 
	Transformer transformer = tfactory.newTransformer(); 
	Properties prop = new Properties();
	prop.setProperty("encoding", "Shift_JIS");
	transformer.setOutputProperties(prop);
	transformer.transform(new DOMSource(doc),
            new StreamResult(System.out));
    }
    private static HashSet<String> getIdSet(NodeList list){
	HashSet<String> idset = new HashSet<String>();
	for(int i=0; i<list.getLength(); i++){
	    Element e = (Element) list.item(i);
	    if(e.hasAttribute("id")){
		idset.add(e.getAttribute("id"));
	    }
	}
        return idset;
    }
    private static void appendId(Document doc){
	NodeList list= doc.getElementsByTagName("*");
	HashSet<String> idset = getIdSet(list);
	int id=0;
	for(int i=0; i<list.getLength(); i++){
	    Element e = (Element) list.item(i);
	    if(!e.hasAttribute("id")){
		String idstr;
		do{
		    idstr=String.valueOf(id++);
		}while(idset.contains(idstr));
		e.setAttribute("id",idstr);
	    }
	}
    }
    public static void main(String[] args) throws Exception {
	Document doc = getDocument(System.in);

        appendId(doc);
	
        printDocument(doc);
    }
}

サンプル XML

<?xml version="1.0" encoding="shift_jis" ?>
<class name="データ構造とアルゴリズム II" >
<teacher name="坂本直志" />
<time id="0" day="Tues" period="2" />
</class>

XML の作成

最後にテキストファイルを読んで XML ファイルを出力する例を示します。 ここでは、 Excel などで作成できる CSV(Comma Separated Values) を読んで、それに相当する XML を作成します。

CSV は次のようなファイルです。

"品名","単価","個数","合計"
"りんご",200,3,600
"みかん",100,5,500
"もも",300,1,300

これは次のような文法で示せます。

CSV ::= (line)*
line ::= data (, data)*
data ::= number | """ string """

これを JavaCC で解釈しても良いですが、単純に "(ダブルクォート) と ,(カンマ) を含まない文字列を取り出すという正規文法で要素を取り出すこと にします。 つまり次の正規文法で要素を取り出します

[^,"]+

但し、この方法では「1,,3」など空の項目が存在する時に、要素数が 2 個しかな いという誤動作をします。 ちゃんと構文解析するには、[^,]* で要素を取り出した後、 [^"]*でデータを取り出す必要があります。 二段階に分けずに [^,"]* で取り出すと、「1,"2",3」というデータに対して、 「(1),()"(2)"(),(3)」とカッコで囲んだように 5 箇所にマッチしてしまいま す。

次に生成する XML ですが、上記の CSV の一般形を考えます。 最初の行は各データの項目名が含まれていて、次の行からはその項目名に対す るデータが含まれているとします。 例えば、上記のデータは次のように変換されるようにしたいと考えます。

<itemlist>
  <item>
    <data name="品名" value="りんご" />
    <data name="単価" value="200" />
    <data name="個数" value="3" />
    <data name="合計" value="600" />
  </item>
  <item>
    <data name="品名" value="みかん" />
    <data name="単価" value="100" />
    <data name="個数" value="5" />
    <data name="合計" value="500" />
  </item>
  <item>
    <data name="品名" value="もも" />
    <data name="単価" value="300" />
    <data name="個数" value="1" />
    <data name="合計" value="300" />
  </item>
</itemlist>

これの DTD と XML Schema の定義を示します。 但し、一行目にメタデータが含まれていて、二行目以降はデータということだ け決め、データがいくつあるかどうかなどは問わないとします。 つまり、決めなければいけないのは、行単位でデータが存在することと、各デー タに名前がついていることだけです。

DTD

<!ELEMENT itemlist (item*)>
<!ELEMENT item (data*)>
<!ELEMENT data EMPTY>
<!ATTLIST data name CDATA #REQUIRED
               value CDATA #REQUIRED>

XML Schema

<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <xsd:element name="itemlist" type="itemlistType"/>
  <xsd:complexType name="itemlistType">
    <xsd:sequence>
      <xsd:element name="item" type="itemType"/>
    </xsd:sequence>
  </xsd:complexType>
  <xsd:complexType name="itemType">
    <xsd:sequence>
      <xsd:element name="data" type="dataType"/>
    </xsd:sequence>
  </xsd:complexType>
  <xsd:complexType name="dataType">
    <xsd:attribute name="name" type="xsd:string"/>
    <xsd:attribute name="value" type="xsd:string"/>
  </xsd:complexType>
</xsd:schema>

さてこの変換プログラムを作成します。

  1. プログラムの最初に itemlist ノードを作成します。
  2. 最初の行を読み込み、各要素を取り出し配列に格納します。 但し、要素数があらかじめわかっていないので、 java.util.ArrayList に格 納します。
  3. 各行を読み込む時、 item に対応する Element ノードを作成し、 itemlist に加えます。
  4. 正規表現で順に要素を読み込み、 data Element を生成します。 要素名とデータを付加して item ノードに追加します。
  5. データを読み終えたら XML を出力します。

import java.io.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import org.w3c.dom.*;
import java.util.*;
import java.util.regex.*;
class Rei {
    private static Document getDocument() throws Exception {
	DocumentBuilderFactory dbf
	    = DocumentBuilderFactory.newInstance();
	DocumentBuilder db = dbf.newDocumentBuilder();
	return db.newDocument();
    }
    private static void printDocument(Document doc) throws Exception {
	TransformerFactory tfactory = TransformerFactory.newInstance(); 
	Transformer transformer = tfactory.newTransformer(); 
	Properties prop = new Properties();
	prop.setProperty("encoding", "Shift_JIS");
	transformer.setOutputProperties(prop);
	transformer.transform(new DOMSource(doc),
            new StreamResult(System.out));
    }
    private static ArrayList<String> getDataName(String line){
	ArrayList<String> dataName = new ArrayList<String>();
	Matcher m = p.matcher(line);
	while(m.find()){
	    dataName.add(m.group());
	}
        return dataName;
    }
    private static Element createData(String name, String value){
	Element data = doc.createElement("data");
	data.setAttribute("name",name);
	data.setAttribute("value",value);
        return data;
    }
    private static Element getItem(List<String> dataName, String line){
	Element item = doc.createElement("item");
	Matcher m = p.matcher(line);
	int index=0;
	while(m.find()){
            item.appendChild(createData(dataName.get(index++),m.group()));
	}
        return item;
    }
    private static Document doc;
    private static Pattern p;
    public static void main(String[] args) throws Exception {
        p = Pattern.compile("[^\",]+");
	doc = getDocument();

	Element root = doc.createElement("itemlist");
	doc.appendChild(root);
	BufferedReader br = new BufferedReader(
			    new InputStreamReader(System.in,"Shift_JIS"));
	String line;
	if((line=br.readLine())==null){
	    return ;
	}
	ArrayList<String> dataName = getDataName(line);

	while((line=br.readLine())!=null){
            root.appendChild(getItem(dataName,line));
	}
        printDocument(doc);
    }
}

8-3. 付録

OMG IDL

OMG は Object Management Group という団体です。 この団体が提案したコンピュータネットワーク上で使用する分散オブジェクト のための規格 CORBA(Common Object Request Broker Architecture)で使用す るインターフェイス記述言語が OMG IDL になります。 言語仕様は CORBA の仕様書の http://www.omg.org/technology/documents/formal/corba_2.htm 3 章に割り当てられていて、ダウンロード可能です。


坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科