[XML]要素・属性の使い分けと命名規則(要素名・属性名の決め方)

プログラミングをする上でデータを格納する標準的な形式として XML はよく使われます。
XML には値を入れておくための要素(Element)と属性(Attribute)があり、どちらを使わなければいけないという明確な決まりはありません。
しかしながら何らかのルールに従って要素に格納するか属性を使うかを決め、ファイル全体に一貫性を持たせなければいけません。

<要素 属性="属性の値">要素の内容</要素>

自分一人でルールを決めるよりも一般論を取り入れたほうが無難です。私自身要素・属性の使い分けに悩むことが多いので次のガイドラインを参考にルールについて考えてみたいと思います。

Google XML Document Format Style Guide
https://google.github.io/styleguide/xmlstyle.html

Principles of XML design: When to use elements versus attributes
http://www.ibm.com/developerworks/library/x-eleatt/


要素名・属性名の大文字・小文字

長い要素名を区切る場合、半角スペースは使えないのでそのまま続けて書くか、単語の頭を大文字にする、アンダースコア(_)を利用することになります。

<sampleelement>
<sampleElement>
<SampleElement>
<sample_element>

全て文法としては正しいです。Google のガイドラインは All names MUST use lowerCamelCase. と定めていますし、他の多くの XML 文章を見てもそうなっていることが多いので、ここは「sampleElement」としておくのが良さそうです。

ちなみに「URL」のような略語は「sourceURL」のようにせず「sourceUrl」としたほうが良いようです。しかし「iPhone」や「iMac」をどう処理すべきかは難しいところです。「sampleiPhone」のようにしてしまっては可読性が下がってしまうので、ガイドラインにはありませんが「sampleIphone」とするか「sampleIPhone」のようにしておくしかなさそうですね。

属性に関しても要素と同じ記法を使うのが良さそうです。XHTML などでは属性のみ http-equiv のようにハイフンで書くようにしている場合も多く見られますが特に理由がなければこちらも lowerCamelCase で良いと思います。

要素・属性の使い分け

一般的には要素より属性のほうが制限が多く、長すぎる内容を入れたり改行を含むものを入れるには向きません。要素であれば CDATA セクションも利用できるのであらゆる値は要素だけでも一応表現できます。

ただし、メモリ消費の観点で見ると、要素は同じ要素を複数含む可能性があるため探索に多くのメモリを消費しますが、属性は2つ以上同じ名前を持てないため、少ないメモリで済みます。
とはいえ一つの要素に多くの属性をもたせるのはあまり一般的ではなく、せいぜい 10 個程度にして相関性の強いものを子要素でグループ化してそちらの属性とするのが良いようです。

方向性としては利用者の目に触れるものは要素、管理のために使われる内部的なデータは属性にするのが良いようです。ただし Google は「このルールは別の理由で破られることが多い」と注釈を添えています。

ガイドライン(12. Elements vs. Attributes)によるとおおまかな基準は次のとおりです。(要約&抜粋)

要素に向いているケース

・データが複数回出現するなら要素を使う。「data1=”..” data2=”..”」のようにはしない。
・一つのオブジェクトとしてまとめることができ、親子関係があるなら要素を使う。
・出現順序に意味がある場合
・複数行のデータ
・データが巨大な場合や BASE64 変換されたバイナリなど。
・内容が自然言語であれば要素に入れ、属性として使用言語を記述するのが良い。

属性に向いているケース

・管理用のIDやコード番号は属性にする。
・別のデータのクラス、役割、処理方法を表すメタデータの場合。
・上書きされない限り子孫要素にも属性の内容が反映される場合(xml:lang や 名前空間宣言など)

値の記述

key-value で表すことのできるデータは要素名としてキー、属性 value として value を記述するのが良いとされています。

<genre value="science" />

また、単位系には 国際単位 を使うのが一般的で、できれば単位を表す属性 unit をつけておくのが理想です。

<weight value="180" unit="kg" />

まとめ

要素名・属性名は lowerCamelCase で記述し、ユーザーに見せる内容は要素に、管理・検索に使う内部的な値は属性に持たせる。
key-value のペアで表せるような単純なものであれば空要素を用いて要素名と value 属性の形で表現する。

ただし Google のガイドラインでは次のように締めくくられています。(拙訳)

“ルールの奴隷となって粗雑で独りよがりのむかむかするような汚らしい設計を作るよりはルールの一部、あるいは全てを破ってしまおう。(たとえそれが MUST とされていたとしても。)特に「シンプルなもの/複雑なもの」「メタデータ/データ」のようにはっきり分けられるなら 子要素・属性を両方使ったほうが良いが、不規則に混在させてしまうと従うのも使うのも難しくなってしまう。”

PHP で XML の内容を取得する(DOM, XPath)

前回、基本的なファイルの読み書きに関してまとめたので、今回は XML の要素を取得したり、検索する方法についてのメモです。

要素を名前から取得するには、getElementsByTagName(“要素名”) を使います。 <data> ノードの中にある最初の <sample> ノードの内容を表示するサンプルがこちらです。

sample.xml
<?xml version="1.0" encoding="utf-8"?>
<data>
  <sample id="1">red</sample>
  <sample id="2">green</sample>
  <sample id="3">test</sample>
  <sample id="4">blue</sample>
  <sample id="5">white</sample>
  <sample id="6">yellow</sample>
</data>
PHP
<?php
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->load("sample.xml");

$root       = $dom->getElementsByTagName("data")->item(0);
$sampleNode = $root->getElementsByTagName("sample")->item(0);

echo $sampleNode->nodeValue;
結果
red

getElementsByTagName() を使うと、その親ノードより深いすべてのノードが配列として取得されます。 一番最初(0番目)のアイテムを選択する場合は「item(0)」のように指定します。
ノードから値を取り出す時は「nodeValue」を使えば取得できます。ノードの内容を書き換える際も直接 nodeValue 書き換えます。

$sampleNode->nodeValue = 'New Text';

上の例ではルートノード <data> を getElementsByTagName() で取得しましたが、 documentElement を使うとルートノードの名前がわからない場合でも取得できます。
ルートノードとはドキュメント内で最も浅い親ノードのことで、この場合は <data> です。 よって下記のように記述しても同じ結果を得ることができます。

$root   = $dom->documentElement;
$sample = $root->getElementsByTagName("sample")->item(0)->nodeValue;

ノードの検索

複数ある <sample> 要素の内、内容が「test」であるノードを取得してみます。

<?php
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->load("sample.xml");
$root = $dom->documentElement;

$search = "test";
$result = null;

$sampleNodeList = $root->getElementsByTagName("sample");
foreach($sampleNodeList as $sampleNode){
  if($sampleNode->nodeValue === $search){
    $result = $sampleNode;
    break;
  }
}

if( !is_null($result) ) echo $result->getAttribute("id");
結果
3

サンプルの $sampleNodes の中には getElementsByTagName() で見つかった全ての sample ノードを格納されています。 foreach でループさせ、値が見つかったら break でループを抜けます。 見つかった要素の属性名「id」の内容を取得するには getAttribute(“属性名”) を使います。


XPath を使ったノード探索

上記の方法が間違っているというわけでは有りませんが、ループで検索するのは原始的すぎると思うのであれば、XPath を使うと綺麗にまとまります。

<?php
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->load("sample.xml");

$xpath = new DOMXPath($dom);

$result = $xpath->query("/data/sample[text()='test']")->item(0);
echo $result->nodeValue;

随分すっきりしました。
基本的には query() に検索条件を指定して抽出します。 サンプルでは、値「test」を持つ sample ノードを取得しています。 「/」は階層を表します。「/data/sample」の時、<data> 要素が持つ子ノードの内、<sample> という名前の要素を全て選択します。
ディレクトリパスの書き方と同様に「.」は現在のノードを表し、「..」と書けばその親を指定することも出来ます。
条件は [] の中に記述します。テキスト内容は text() で得られるので、「text()=’test’」とするとテキストが「test」である場合という条件になります。
n 番目に見つかったノードという条件であれば「/data/sample[1]」のように指定できます。数値は 0 ではなく 1 から始まるため、この場合最初に見つかった sample ノードという意味になります。「/data/sample[position()=1]」と書いても同じです。最後に見つかったノードであれば「/data/sample[last()]」という書き方ができます。

id などの属性(Attribute)からノードを選択する場合は属性名の前に @ をつけて指定します。
ドキュメント内の全ての <sample> のうち、id が 3 であるものを取得してみます。

<?php
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->load("sample.xml");

$xpath = new DOMXPath($dom);
$result = $xpath->query('//sample[@id = "3"]')->item(0);
echo $result->nodeValue;

条件は「and」「or」を使って複数指定することもできます。

[@id="3" or @class="sample"]

この場合 id が 3 又は クラスが sample であるという条件になります。

XPathクエリの「/」は子ノード(一つ下の階層)までを検索対象にしますが、クエリに「//」をつかうとそのノード以下の子孫ノード(すべての階層のノード)が対象になります。「/data//sample」のように探索することも出来ます。この場合は <data> 以下の全ての深さの階層から <sample> という名前のノードを選択することになります。

条件は and や or で組み合わせることができます。

$result = $xpath->query('//sample[text()="green" or text()="red"]');
foreach($result as $node){
  echo $node->nodeValue . "<br />";
}

「red green」と表示されます。 red か green の値を持つすべての sample ノードが抽出されているのがわかります。

XPath の query() によって取得した DOMElement から、さらに query を使って絞り込む場合、query() の第2引数として絞込元となるノード(コンテキストノード)を渡します。そのさいクエリを「//title」のようにしてしまうとコンテキストノードを指定する意味がなくなってしまうので、「.//title」などの相対的な書き方にします。

<?php
$xml = <<<EOD
<?xml version="1.0" encoding="utf-8"?>
<data>
  <article>
    <title>sample1</title>
  </article>
 
  <article>
    <title>sample2</title>
  </article>
</data>
EOD;
 
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->loadXml($xml);
 
$xpath = new DOMXPath($dom);
 
$article = $xpath->query('//article')->item(0);
$title = $xpath->query('.//title', $article)->item(0);
echo $title->nodeValue;
結果: sample1

XPath の文法の詳細については、こちらのサイト(http://www.techscore.com/tech/XML/XPath/xpath02.html/)が わかりやすくまとめてくださっています。


子ノードの削除

親ノードから子ノードを削除するには removeChild() を使います。

$xpath  = new DOMXPath($dom);
$result = $xpath->query('/data/sample[text()="test"]')->item(0);
$result->parentNode->removeChild($result);

$root->removeChild($result);

子ノードの削除は親ノードから行います。親ノードの参照は parentNode に格納されているのでそれを利用します。あるいは '/data/sample/..' のようにしてパスを遡って親ノードを取得する方法もあります。

PHP で XML を読み書きする (DOM)

ちょっとしたデータの保管や、RSS、サイトマップなどを出力する際、
XML を扱うことはかなり多いと思いますが、
実際に操作してみるとなんとなくややこしいように見えるので
基本的な方法をまとめておきます。

新規に XML を作るには、新しくDOM ドキュメントを作ります。
「1.0」は XML のバージョンで、「UTF-8」は文字コードの指定です

$dom = new DOMDocument('1.0', 'UTF-8');

ファイルから読み込む場合も新しい DOM ドキュメントを作り、
その中にファイルの内容を読み込みます。

$dom = new DOMDocument('1.0', 'UTF-8');
$dom->load("sample.xml");

すでに変数の中に文字列として XML の内容を読み込んである場合は
loadXML() を利用します。

$string = file_get_contents("sample.xml");
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->loadXML($string);

このままでも読み込みとしては問題ないのですが、
保存時や出力時に改行やインデントがなくなるため、
整形された XML を出力したい時は次のようにします。

$dom = new DOMDocument('1.0', 'UTF-8');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->load("sample.xml");

ノードを追加するには、createChild() でノードを作成し、
appendChild でノードを追加するのが基本的な流れです。

$datanode = $dom->createElement("data");
$root = $dom->appendChild($datanode);

この場合は空っぽのノード <data /> が作られます。
ではこの親ノードに値を持つ子ノードを追加してみます。

$childnode = $root->appendChild($dom->createElement("child", "Hello"));

今回は省略して一行にまとめてみましたがやっていることは同じです。
実行結果は下のようなります。

<?xml version="1.0" encoding="UTF-8"?>
<data>
  <child>Hello</child>
</data>

テキストノードの代わりに CDATASection を挿入することも出来ます。
CDATASection内部では「<>」などの記号をエスケープすることなく扱うことが出来ます。
通常のテキストノードであっても自動的に「&lt;」のように変換されるので問題は有りません。

$cdata = $dom->createCDATASection("Hello");
$childnode->appendChild($cdata);
<child><![CDATA[Hello]]></child>

次は実行結果をファイルに保存します

$dom->save("test.xml");

画面に表示したり、変数に格納する場合は次のようにします

$content = $dom->saveXML();
header("Content-Type: text/xml; charset=utf-8");
echo $content;

新しい XML を表示するサンプル

<?php
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;

$datanode = $dom->createElement("data");
$root = $dom->appendChild($datanode);

$root->appendChild($dom->createElement("child", "Hello"));
$content = $dom->saveXML();
header("Content-Type: text/xml; charset=utf-8");
echo $content;

ノード値の取得、編集、検索などの方法は次回で説明します。