Java : XML (DOM) の基本操作
XML の構文は知っているけど、Javaで扱うにはどうしたらよいのかな…
そんなかたを対象に、本記事では XML を扱う基本となる DOM (Document Object Model) API の使い方について解説します。
対象読者:XML の構文は理解しているかた
概要
JavaでXMLを扱うには、大きく分けて3つの種類のAPIがあります。
API | 簡単な説明 | 向き | 不向き |
---|---|---|---|
DOM | XMLを、アプリから操作しやすいツリー構造で表現します。 アプリはツリー構造の各要素(ノード)に対して読み・書き・追加・削除ができます。 |
比較的サイズの小さなXMLに対して、きめ細やかな操作をしたいときに向いています。 | XMLをまるごとツリー構造として表現するため、巨大なXMLを扱うとメモリ不足になる場合があります。 |
SAX | XMLを読み込み(パース)しながら、逐次的にアプリケーションで処理します。 アプリはSAXのイベントを受動的に受け取ります。 PUSH型とも言われています。 |
単純な構造のXMLの読み込みに向いています。 ツリー構造を作らないので、巨大なサイズのXMLでもメモリ使用量は抑えられます。 |
XMLの先頭から順々に処理するため、例えば最後のノードを処理してから最初のノードを処理したい、というのは難しいです。 |
StAX | SAXとは違い、アプリはイベントを能動的に取得します。 PULL型とも言われています。 SAXより後発のAPIなこともあり、SAXより使いやすくなっている、と思います。(そこまでがっつりと使ったことはないですが…) |
SAXと同じです。 | SAXと同じです。 |
本記事では、この中の DOM (Document Object Model) を解説していきます。
DOMのAPIは W3C によって勧告されています。Java独自の仕様ではありません。
そのため、Java以外のプログラミング言語(例えばC#やPythonなど)でも実装されています。
JavaでDOMが理解できれば、他の言語でもほぼ同じ使い方ができます。
なので理解しておいて損はないかな、と。
W3C (World Wide Web Consortium)
- Webに関する各種技術の標準化を推進する団体です。
HTMLやXML、DOMといった規格が勧告されています。 - Java 22 の時点では、DOM Level 3 Core の仕様に対応しています。
Document Object Model (DOM) Level 3 Core Specification
JAXP セキュリティ・ガイド
少し難しい内容ですが、大事なことなので早めにご紹介してしまいます。
今はまだ読まなくてもよいですが、XMLの扱いに慣れてきたら下記の公式ドキュメントもご確認ください。
Java API for XML Processing (JAXP)セキュリティ・ガイド (Java SE 22)
XML処理中の潜在的な攻撃である
- XML外部エンティティ(XXE)インジェクション攻撃
- 指数関数的エンティティ展開攻撃
について解説しています。
また、それらの攻撃を回避するためのXMLConstants.FEATURE_SECURE_PROCESSINGやプロパティについても詳しく説明されています。
外部からの信頼されていないXMLを読み込む場合は、上記のページを熟読することをおすすめします。
XMLテキスト → DOM
DOMでは、XMLをツリー構造に表現して操作していきます。
XMLテキストからDOMのツリー構造を生成することを、パース(Parse)といいます。
Parse = 解析ですね。
それではコード例を見てみましょう。
final var xml = """
<root><child-a/><child-b/></root>
""";
final var builderFactory = DocumentBuilderFactory.newInstance();
final var builder = builderFactory.newDocumentBuilder();
final var document = builder.parse(new ByteArrayInputStream(xml.getBytes()));
final Element root = document.getDocumentElement();
System.out.println(root); // [root: null]
final var nodes = root.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
final var node = nodes.item(i);
//i = 0 : [child-a: null]
//i = 1 : [child-b: null]
System.out.println("i = " + i + " : " + node);
}
XML形式の文字列をパースしてDOMのツリー構造を生成します。
そして、ツリー構造の内容を確認しています。
コードを少しずつ見ていきましょう。
DOMのツリー構造では、ツリーの一番の親は Document となります。
そのため、Document を生成するための DocumentBuilder を生成するための DocumentBuilderFactory から生成していきます。
// DocumentBuilerを生成するためのFactoryを生成します。
final var builderFactory = DocumentBuilderFactory.newInstance();
// FactoryでDocumentBuilderを生成します。
final var builder = builderFactory.newDocumentBuilder();
// DocumentBuilderでDocumentを生成します。
// 対象となるXMLはInputStreamとして読み込ませています。(他にはURIやファイルからなども可能)
final var document = builder.parse(new ByteArrayInputStream(xml.getBytes()));
Document は、ルートとなる1つの Element を持ちます。
final Element root = document.getDocumentElement();
System.out.println(root); // [root: null]
Element は複数の子ノードを持ちます。
final var nodes = root.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
final var node = nodes.item(i);
//i = 0 : [child-a: null]
//i = 1 : [child-b: null]
System.out.println("i = " + i + " : " + node);
}
ツリー構造は次のようなイメージになります。
DOM → XMLテキスト
DOMからXMLテキストに変換するには Transformer を使います。
Transformer は Java独自のAPI です。
DOM専用というわけではなく、SAX や StAX、文字列にも使える汎用的なAPIとなっています。
final var xml = """
<root><child>aaa</child></root>
""";
final var builderFactory = DocumentBuilderFactory.newInstance();
final var builder = builderFactory.newDocumentBuilder();
final var document = builder.parse(new ByteArrayInputStream(xml.getBytes()));
final var transformerFactory = TransformerFactory.newInstance();
final var transformer = transformerFactory.newTransformer();
final var source = new DOMSource(document);
final var result = new StreamResult(new StringWriter());
transformer.transform(source, result);
//<?xml version="1.0" encoding="UTF-8" standalone="no"?><root><child>aaa</child></root>
System.out.println(result.getWriter());
XMLテキストをまずDOM形式にパースして、それを Transformer で再びXMLテキストへと変換する例となります。
コードを少しずつ見ていきましょう。
// Transformerを生成するためのFactoryを生成します。
final var transformerFactory = TransformerFactory.newInstance();
// FactoryでTransformerを生成します。
final var transformer = transformerFactory.newTransformer();
DocumentBuilder のときと同じように、Transformer を生成するためにまず Factory を生成します。
Transformer.transform で実際に変換します。
Source → Result へと変換されます。
// SourceとしてDOMを使います。
final var source = new DOMSource(document);
// 結果はストリーム(文字列)で受け取ります。
final var result = new StreamResult(new StringWriter());
// 変換します。
transformer.transform(source, result);
Source と Result は SAX や StAX などにも対応しています。
詳細は API仕様をご参照ください。
//<?xml version="1.0" encoding="UTF-8" standalone="no"?><root><child>aaa</child></root>
System.out.println(result.getWriter());
無事、StringWriter 経由で結果を文字列として取得できました。
<?xml ~ ?>
の部分は、XML宣言といいます。
Tranformer ではデフォルトで出力します。
もし出力させたくない場合は、次の setOutputProperty で設定できます。
出力形式の設定
Transformer.setOutputProperty を使うことで、出力形式を設定できます。
nameパラメータに指定できる文字列は、OutputKeys クラスに定義されています。
よく使うのは、
- OMIT_XML_DECLARATION
- INDENT
あたりでしょうか。
OMIT_XML_DECLARATION の例です。
final var xml = """
<root><child>aaa</child></root>
""";
... 省略 ...
final var transformer = transformerFactory.newTransformer();
// デフォルトは"no"
System.out.println(transformer.getOutputProperty(OutputKeys.OMIT_XML_DECLARATION)); // "no"
// "yes"に変更
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
final var source = new DOMSource(document);
final var result = new StreamResult(new StringWriter());
transformer.transform(source, result);
//<root><child>aaa</child></root>
System.out.println(result.getWriter());
OMIT_XML_DECLARATION を "yes" に設定すると、XML宣言の <?xml ~ ?>
は出力しません。
INDENT の例です。
final var xml = """
<root><child>aaa</child></root>
""";
... 省略 ...
final var transformer = transformerFactory.newTransformer();
// デフォルトは"no"
System.out.println(transformer.getOutputProperty(OutputKeys.INDENT)); // "no"
// "yes"に変更
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
final var source = new DOMSource(document);
final var result = new StreamResult(new StringWriter());
transformer.transform(source, result);
//<?xml version="1.0" encoding="UTF-8" standalone="no"?>
//<root>
// <child>aaa</child>
//</root>
System.out.print(result.getWriter());
見やすいように、改行とスペースをいい感じに入れてくれます。
ただし、1点注意があります。
インデントされたXMLテキストをそのままパースすると、追加された改行とスペースは Textノードとしてツリー構造に追加されます。
final var xml = """
<root>
<child>aaa</child>
</root>
""";
final var builderFactory = DocumentBuilderFactory.newInstance();
final var builder = builderFactory.newDocumentBuilder();
final var document = builder.parse(new ByteArrayInputStream(xml.getBytes()));
final var root = document.getDocumentElement();
final var nodes = root.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
final var node = nodes.item(i);
// 見やすさのため、半角スペースは * 改行は \n として表記します。
//i = 0 : [#text: \n****]
//i = 1 : [child: null]
//i = 2 : [#text: \n]
System.out.println("i = " + i + " : " + node);
}
ノードの操作
今までの例では、XMLテキストをパースすることにより、Nodeのツリー構造を作成してきました。
もちろん、XMLテキストなしで、1から Nodeのツリー構造を作成することも可能です。
コード例を見てみましょう。
final var builderFactory = DocumentBuilderFactory.newInstance();
final var builder = builderFactory.newDocumentBuilder();
// 空のDocumentを生成します。
final var document = builder.newDocument();
// ルートとなるElementを作成して、documentに追加します。
final var root = document.createElement("root");
document.appendChild(root);
// 子となるElement(child-a)を作成して、rootに追加します。
final var childA = document.createElement("child-a");
root.appendChild(childA);
// Textノードを作成して、child-aに追加します。
final var text = document.createTextNode("aaaa");
childA.appendChild(text);
// 子となるElement(child-b)を作成して、rootに追加します。
final var childB = document.createElement("child-b");
root.appendChild(childB);
// 属性ノード bbb="ccc" を作成して、child-bに設定します。
final var attr = document.createAttribute("bbb");
attr.setValue("ccc");
childB.setAttributeNode(attr);
final var transformerFactory = TransformerFactory.newInstance();
final var transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
final var source = new DOMSource(document);
final var result = new StreamResult(new StringWriter());
transformer.transform(source, result);
//<root>
// <child-a>aaaa</child-a>
// <child-b bbb="ccc"/>
//</root>
System.out.print(result.getWriter());
Document.create~で各種ノードを生成し、appendChild で子ノードとして追加していく、というのが基本となります。
Node
Node は、すべてのツリー構造の要素の基底となるインタフェースです。
ただし、属性(Attr)は少し特殊で、ツリー構造の一部とはならずに Element に直接保持されます。
どのノードがどのノードを子ノードとして持てるのか? もしくはノードを持てないのか?というのは上記の DOM仕様で定義されています。
主要なノードのみに絞ると次のようになります。
- Document -- Element (maximum of one), Comment
- Element -- Element, Text, Comment
- Attr -- Text
- Comment -- no children
- Text -- no children
Comment や Text は子ノードを持てないことが分かりますね。
Node の主要なメソッドは、ツリー構造に関するものです。
親ノードを取得(getParent)、子ノードを追加(appendChild)、子ノードを取得(getChildNodes) などがあります。
API | コード例 |
---|---|
short getNodeType() 自分がなんのノードなのか?というタイプ値を返します。値については定数で定義されています。 |
|
Node getParentNode() 親ノードを取得します。 |
|
Node getFirstChild() 先頭の子ノードを取得します。 |
|
Node getLastChild() 最後の子ノードを取得します。 |
|
NodeList getChildNodes() 子ノードをすべて取得します。(孫となるノードは取得されません) |
|
Node appendChild(Node newChild) 指定したノードを、子ノードの最後に追加します。 |
|
Node insertBefore(Node newChild, Node refChild) すでに追加済みのNodeの手前に、新しくノードを追加します。 |
|
NodeList
Node.getChildNodes や Element.getElementsByTagName などで、複数の Node を返すときに使われます。
Java標準API の Listインタフェース ではないので、少し使いづらいですね。(拡張for文も使えません)
素直に、通常のfor文でループさせるのがよさそうです。
final var xml = """
<root><child-a/><child-b/><child-c/></root>
""";
final var builderFactory = DocumentBuilderFactory.newInstance();
final var builder = builderFactory.newDocumentBuilder();
final var document = builder.parse(new ByteArrayInputStream(xml.getBytes()));
final var root = document.getDocumentElement();
System.out.println(root); // [root: null]
final var nodes = root.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
final var node = nodes.item(i);
//i = 0 : [child-a: null]
//i = 1 : [child-b: null]
//i = 2 : [child-c: null]
System.out.println("i = " + i + " : " + node);
}
Document
Document は、DOMのツリー構造の一番親となるノードです。
ルートとなる1つの Elementノードを持ちます。
そして、Element や Text、Attr といった各種ノードを生成する役割をもちます。
API | コード例 |
---|---|
Element getDocumentElement() ルートとなるElementノードを取得します。 |
|
Element createElement(String tagName) 指定したタグ名のElementノードを生成します。 |
|
Attr createAttribute(String name) 指定した属性名のAttrノードを生成します。 |
|
Text createTextNode(String data) 指定した内容のTextノードを生成します。 |
|
Comment createComment(String data) 指定した内容のCommentノードを生成します。 |
|
Element
Element は、XML構造の 要素 を表します。
子ノードとして Element、Comment、Text などを複数持つことが可能です。
また、子ノードとしてではないですが、属性(Attr)を複数持つことが可能です。
主要なメソッドとコード例になります。
API | コード例 |
---|---|
String getTagName() タグ名を取得します。 |
|
NodeList getElementsByTagName(String name) 子ノードから、指定したタグ名のノードを探して、すべて取得します。子ノード、さらにその子ノードと、再帰的に検索します。 |
|
void setAttribute(String name, String value) 新しい属性を追加します。もしくは、すでに同じ属性名があるのであれば上書きします。 |
|
String getAttribute(String name) 指定した属性名の値を取得します。 |
|
void removeAttribute(String name) 指定した属性名の属性を削除します。 |
|
Attr setAttributeNode(Attr newAttr) Attr getAttributeNode(String name) Attr removeAttributeNode(Attr oldAttr) 属性ノードとしても、set/get/removeできます。 |
|
これ以外にも、Node で定義されている appendChild や getChildNodes もよく使いますね。
Attr
Attr は要素の属性を表します。
ツリー構造にはならず、Element に直接保持されます。
API | コード例 |
---|---|
String getName() 属性名を取得します。 |
|
String getValue() 属性値を取得します。 |
|
void setValue(String value) 属性値を設定します。 |
|
Text
Textノードは、XMLのテキストを表します。
API | コード例 |
---|---|
String getData() テキストの内容を取得します。 |
|
void setData(String data) テキストの内容を設定します。 |
|
Comment
Commentノードは、XMLのコメント要素である <!-- ~ -->
を表します。
API | コード例 |
---|---|
String getData() コメントの内容を取得します。 |
|
void setData(String data) コメントの内容を設定します。 |
|
まとめ
XML は一時期に比べて使用率が減ってきた印象はあります。
代わりに、よりシンプルな JSON が人気でしょうか。
しかし、今回は紹介できませんでしたが、XML はスキーマ言語によって厳格にフォーマットを定義できます。
他にも XSLT で結果を変換できたりと機能が豊富です。まだまだ XML がすたれることはないでしょう。
本記事では扱いきれなかったものとして、
- SAX
- StAX
- DTD
- XML Schema
- XSLT
- XPath
などがあります。
興味のあるかたは、それらのキーワードで調べてみてみるのもおすすめです。
いずれ記事にもまとめられたらな、と思っています。
DOM のさらなる詳細が知りたいかたは、関連記事の各種 API使用例も参考にしていただけたら幸いです。