電子書籍の厳選無料作品が豊富!

<?xml version="1.0"?>
<root>
<items>
 <item>
  <id>1</id>
  <name>商品(赤)</name>
  <price>1500</price>
  <color>red</color>
  <size>8</size>
  <option>A</option>
 </item>

 <item>
  <id>2</id>
  <name>商品(青)</name>
  <price>700</price>
  <color>blue</color>
  <size>3</size>
 </item>

 <item>
  <id>3</id>
  <name>商品(緑)</name>
  <price>1200</price>
  <color>green</color>
  <size>5</size>
  <option>C</option>
 </item>
</items>
</root>

上記のようなXMLデータをPHPでパースし、
1つずつ、配列に入れていきたいと考えていますが、
問題なのは、
上から2番目の商品(青)には、<option>データが無いということです。
これは、この<option>が、オプションが有る時にだけ存在するタグだからです。

//-----------------------------

<?php
$doc->load($url); //上記xmlデータのあるURL:$url
$array = array();

$i = 1;
while($i <= 3){  //実際には、もっと商品数はあります。ここでは例として、3。
 //配列の初期化
 $item_array = array();

 $items = $doc->getElementsByTagName('id');
 $item_array['id'] = $items->item($i)->nodeValue;

 $items = $doc->getElementsByTagName('name');
 $item_array['name'] = $items->item($i)->nodeValue;
 //*
 //*同様のコードが続く
 //*
 $items = $doc->getElementsByTagName('option');
 $item_array['option'] = $items->item($i)->nodeValue;
 //*
 //*同様のコードが続く
 //*

 //商品データが詰まった配列を、さらに配列に入れる
 $array[] = $item_array;
}

//全ての商品のデータが詰まった配列を表示する。
print_r($array);
?>
//-----------------------------

このようなことがしたいわけです。
なお、タグ名を$tag_array = array('id','name',,,,,'option',,,)として、
foreach($tag_array as $tag){
 $items = $doc->getElementsByTagName($tag);
 $item_array[$tag] = $items->item($i)->nodeValue;
}
という風にした方が冗長ではないかとは思いますが、
それは、まぁ横に置いておきまして、

このコードで問題なのは、<option>が途中、つまり2番目で抜けると、
3番目の<option>データが、2番目の商品の<option>データとして配列に入ってしまうことです。

上から順に詰めて、配列に格納されてしまい、困っている、ということです。

<option>データが無い場合は、「ナシ」とでもしておきたいのですが、
どうしたら良いでしょうか?

<item>を1つずつ 
 $items = $doc->getElementsByTagName('item');
で取得して、
各<item>の中の、<option>にアクセスする、なんて方法があるとベストなのですが、
そんな方法はありますでしょうか?
firstChildですと、<item>の場合、<id>になってしまうため、<option>にはアクセスできません。
firstChild以降のChildには、どうやってアクセスしたら良いのでしょうね?

どなたか、宜しくお願い致します。

A 回答 (8件)

getElementsByTagNameが返すのは、DOMNodeListクラスで、


http://www.php.net/manual/ja/class.domnodelist.php
DOMNodeListクラスは、lengthというプロパティを持っています。

<?php

$items = $dom->getElementsByTagName('item');

$results();
$item_props = array('id'=>null, 'name'=>null, 'price'=>null, 'color'=>null, 'size'=>null, 'option'=>null);
$item_keys = array_keys($item_props);
for($i=0; $i<$items->length; $i++){

$item_array = array();

/*
下記が返すのはDOMNode
http://www.php.net/manual/ja/class.domnode.php
*/
$item = $items->item($i);

//DOMNodeにはchildNodesプロパティがある
$children = $item->childNodes;

for($j=0; $j<$children->length; $j++){

$child = $children->item($j);

$name = $child->nodeName;

//念のため小文字にしてみる
$name = strtolower($name);

if(in_array($name, $item_keys)){

$item_array[$name] = $child->nodeValue;

}

}

$results[] = array_merge($item_props, $item_array); //初期値にマージしてやればなければ初期値が入る

}

?>
とか、ですかね。
    • good
    • 0
この回答へのお礼

非常に分かりやすい回答をありがとうございます。
xml文書のパースの仕方には色々な方法があるようなので、
質問文では、とりあえず、私のしたいことを書かせて頂きました。

質問文に挙げたxml文書の例は、あくまで1例で、
実際にはもっと階層が深かったりと複雑なため、
どのようなxml文書でも対応できる、根本的な理解と、
それに基づいた方法を教えて頂けると良いなと考えていました。

また、私の好き好き(多くの場合、知識がないことによる食わず嫌い)にも
よるかとは思いますが、
こちらの回答の方法が私には合っていると感じました。

xmlのパースの方法には、
simpleXML、DOM, XPathを使用した方法、などなど
色々あるようですが、
今までの所、三者三様の回答をして頂き、
比較しながら勉強させて頂きました。

どの方法でも、目的は果たせる、
しかし、長所短所がそれぞれにはありそうだなと思っています。
もし、この辺りのことについて、ご存知でしたら教えて頂けると嬉しいです。

それと、細かいことになりますが、数点質問をさせて下さい。

(1)$results(); とありましたが、
これは、配列変数として初期化しているということでしょうか。
初歩的な質問で、すみません。
なぜ初期化しているのか、ではなく、
配列の初期化にはこういう方法もあるんですね、というのが質問の主旨です。

(2)for($i=0; $i<$items->length; $i++){ のところで、
これは、foreach($items as $item){ とすることと意味はほぼ同じだと
考えてもよろしいでしょうか。
$items->length; を使わずに全件処理する場合には、foreachを使いたいかなと
個人的には思ったのですが、$items->length;を使うメリットがもしありましたら、
教えて下さい。

以上、おかげさまで、
NodeとNodelistの違いと、それらの扱い方について理解できたことで、
したかったことができるようになりました。問題解決です。

お礼日時:2011/01/18 22:54

> あらかじめ、どの階層に、どんなタグがあるか分かっている場合に


そのために、DTD や スキーマがあるんだけど、そこまで書く人は少ないわな。

XPath の場合、相対的な指定ができるから(上がることも可能)指標(必要な情報)が分かれば十分なんだけどね。

XPath の魅力を語っても仕方ない。イテレーションするか、ピンポイントで拾うか、XSL、XQuery でまとめて拾うかは、作成者の自由だし。

jQuery はバージョンいくつからか(元々その傾向はあったけれど)、Selecters API を採用している。あれはあれで使いやすい。

イテレーションの場合、Traversal API が使えると一番いいんだけど、PHP のライブラリとしてはまだ存在しないから、地味に 参照を辿っていくしかないのが残念。

何だかんだいって現状は、既存 API のバージョンアップと、各種モジュールの追加に期待するしかない。
    • good
    • 0
この回答へのお礼

お返事が遅くなり、すみませんでした。

私にはまだ難しい用語もいくつかありましたが、
今回、とても勉強になりました。
特にXPathの記述例をいくつか挙げて頂けたことに感謝しています。
またXMLのことで分からないことがありましたら、教えて下さい。

以上、最後まで丁寧に付き合って頂き、本当にありがとうございました。

お礼日時:2011/01/24 07:12

基本的には、文字列操作を行う場合に、文字列を上から順番に読み解いていくのか、正規表現を使うのかの違いぐらいの内容、と思ってもらえればいいのかなと思います。


または、JavascriptでDOMをゴリゴリ操作するのか、jQueryを使うのか、といったことに意味合いとしては似てると思います。

そう考えれば、XPath構文のわかりやすさは、yuu_xさんの仰るように、慣れです。
分かってしまえば、こまわりも効くし扱いやすい。

でも、XPathは、アクセス後に返してくるオブジェクトは、やっぱり基本のDOMNodeListだったりするわけですので、
そこから先の操作は基本のDOMDocumentを使ったときと同じ構文を使うので、
両方の特性を上手く組み合わせて、上手く付き合っていけばいいかなと。


下記、余談です。このXMLやHTMLを操作するためのライブラリって色々あるので、軽くご紹介を。
ライブラリの挙動を追えば、どうやって操作したらやりやすいのかとか、理解も深まるかも。

■XML_Unserializer
XMLを単純に配列に落とすだけだったら、PEARに、XML_Unserializerというライブラリもあって、
コレはもともとPHP4でも使えるように成っているので、DOMDocumentやXPathなんかは使われておらず、
最初に書いたような文字列操作と正規表現で行っています。
URL:http://pear.php.net/manual/ja/package.xml.xml-se …

■CakePHP::XMLクラス
CakePHPも自前で、XMLクラスといったものを搭載してますが、同様な理由(PHP4でも使える)で、
DOMDocumentやXPathは使ってなかったりします。

■PHP Simple HTML DOM Parser
コレもDOMDocumentやXPathなどは使ってないものです。jQueryと同じ構文でかけるので扱いやすい
URL:http://simplehtmldom.sourceforge.net/

■phpQuery
こちらは上記の物とは違ってPHP5用ということで、内部的にはDOMElementを使っているし、XPathも使います。
http://code.google.com/p/phpquery/
ちょいちょいバグがあるので安定性はかけますが・・・
    • good
    • 0
この回答へのお礼

回答ありがとうございます。

>XPathは、アクセス後に返してくるオブジェクトは、やっぱり基本のDOMNodeListだったりするわけですので、
そこから先の操作は基本のDOMDocumentを使ったときと同じ構文を使うので、
両方の特性を上手く組み合わせて、上手く付き合っていけばいいかなと。

なるほど、納得です。
よくわかりました。

また、余談の件も参考にさせていただきます。
大変、勉強になりました。

お礼日時:2011/01/21 00:44

> ゴチャゴチャ


w。
慣れるまではそう見えるかもしれない。どんな言語に対しても言えそうだけど、何が書いてあるか分からないうちは、ただの羅列にしか見えない。

ある程度までなら、省略構文が使えるので、それを利用してもいい。

> ある1つの<item>に対してメソッド実行すると、その<item>の中にある<price>1つが対象となる、

# items の直属の子の文脈順の最初の item の子の price ノード
$xpath->evaluate('/root/items/item[1]/price')->item(0);
// 半強制的に DOMNodeList が返る。最初の price ノードが必要な場合は、やや間抜けだけど、戻り値に対して item(0) を指定する。

# items の直属の子の文脈順最初の item の price の値
$xpath->evaluate('number(/root/items/item[1]/price)');
// PHP の場合、戻り値を指定できないため、キャストの手間が入るのは仕方ない。

# name 直下のみ
$xpath->evaluate('string(/root/items/item[1]/name/text())');

 <item>
  <name>商品<ruby>しょうひん</ruby></name>
 </item>

# id=1 の item
$xpath->evaluate('number(/root/items/item[id=1]/price)');

# 最初の item を軸とし、その直属の子 items を選択
$item = $xpath->evaluate('/root/items/item[1]')->item(0);
$xpath->evaluate('child::items', $item);

 <item>
  <items>
  </items>
 </item>
 <item/>
 <item/>

# $item の次の兄弟 item
$xpath->evaluate('following-sibling::item[1]', $item);

後はマニュアルを見てください。
http://www.w3.org/TR/xpath/ - XML Path Language (XPath) Version 1.0
http://www.w3.org/TR/DOM-Level-3-XPath/xpath.html - Document Object Model XPath
    • good
    • 0
この回答へのお礼

補足、ありがとうございます。
お返事が遅くなり、すみません。

今回、XPathによる方法で対処することはほとんど考えていなかったのですが、
このように分かりやすく解説して頂けると、
そうかXPathも捨てたもんじゃないな、いやむしろ、素晴らしいかも?!
と思えてきてしまいますね。

XPathではなく、DOMDocumentを使う方法で、

$items = $doc->getElementsByTagName('item');
 foreach($items as $item){
  $child_nodes = $item->childNodes;
  foreach($child_nodes as $child){
   //…この階層でのデータ抽出
   foreach(…){
    //…この階層でのデータ抽出
    //…さらに深く掘り下げ、この下の階層にも手を伸ばし、
    //…目的のタグのnodeValueの抽出を行う
   }
  }
 }

のように、今のところは書いているわけですが、
1階層で、複数のタグのデータを抽出できるならともかく、
深い階層まで手を伸ばしているにもかかわらず、
抽出するデータは1つか2つなんて場合には、
foreachの繰り返しばかりが増えて、なんだかコードが冗長になってしまい、
これで良いのだろうかと今、ちょっと疑問に思っています。

このような場合、
DOMDocumentではなく、やはりXPathを使用した方が良いのでしょうかね。

XPathだと、あらかじめ、どの階層に、どんなタグがあるか分かっている場合に、
遠回りせずに、直接そのノードへ手を伸ばせる感じがいいなと思っています。

色々と、すみません。
ご迷惑でなければ、また教えて下さい。

お礼日時:2011/01/20 21:18

■$results();の件


ごめんなさい・・・完全にtypoです・・・・
$results = array();
と書きたかったのでした。

■lengthプロパティを使ってforで回した理由
とりあえずPHPマニュアルのDOMNodeListクラスの最初の説明におきまして、
特にIteratorインターフェイスを実装していたりして無さそうだったので、
プロパティとメソッドから判断して書きました。
よく見たらforeachで回せるんですね。すみません。

■SimpleXMLを使うのかDOMDocumentを使うのかXPathを使うのか
基本的にSimpleXMLは、入力されている各要素が分かっている、既知のフォーマットのときに使うと便利かなと。(RSSのパースとか)

DOMDocumentは、読み込めるのがXMLだけではなく、HTMLも読むことが出来るので、
HTMLを読み込んで、抽出だとか置換だとかをするのに適しています。
丁度javascriptでHTMLを書き換えるような感じで使ったりします。

XPath、ある特定の属性をもつ要素をすべて抽出したいとかといった場合に便利ですね。
<?php
$xpath = new DOMXPath($dom);
$elements = $xpath->query("//*[@class='hoge']"); //class="hoge"のものを全部取得する
?>
などといった記述をすれば、NodeListを返しますので、非常に簡単です。
    • good
    • 0
この回答へのお礼

補足をありがとうございます。
お返事が遅れ、すみませんでした。

>■SimpleXMLを使うのかDOMDocumentを使うのかXPathを使うのか

分かりやすくまとめて頂き、非常に参考になりました。
ありがとうございます。

hogehoge78さんに教えて頂きました方法、
つまり、DOMDocumentを使って、
ループ処理で、ノードリストからノードを抽出し、
その中のノードをノードネームで判定して、
適宜、必要なデータを抜いていく、という方法が、
XML文書のパースの基本だと私は考えていまして、
実際に、その方法で、今、私の目の前にあるXML文書に対しても、
パースを試みようとして、以下のようなコードを書き、
問題なくプログラムを稼働させられています。

//--------------------------------------------
 $items = $doc->getElementsByTagName('item');
 foreach($items as $item){
  $child_nodes = $item->childNodes;
  foreach($child_nodes as $child){
   //…この階層でのデータ抽出
   foreach(…){
    //…この階層でのデータ抽出
    //…さらに深く掘り下げ、この下の階層にも手を伸ばし、
    //…目的のタグのnodeValueの抽出を行う
   }
  }
 }

このコードは、動作的には問題ないのですが、
質問で挙げましたXML文書の例とは異なり、
もっと階層が深かったり、階層の深浅が複雑な文書の場合には、
コードの美しさの観点から、あまりオススメとは言えないのでしょうか?
つまり、
foreachで回すようなコードを書くことになるDOMDocumentを使うよりも、
ピンポイントで必要箇所のみを容易に抜くことが可能なXPathを
使用する方がより実際的なのでしょうか?

それとも、
私に知識がないために、このような考え方しかできていないのでしょうか。
質問で挙げたXML文書が単純だったので、今回は、
DOMDocumentによる方法を教えて下さったのだと思います。
私も当初、それを望んでいましたし、
また、その理解を得たおかげで、
今、こうして新たな疑問に直面できてもいるわけです。

というわけで、まとまりのない文章ですが、
何かアドバイスをして頂けますと、とても嬉しいです。
ご迷惑でなければ、また宜しくお願い致します。

お礼日時:2011/01/20 21:43

XPath の利点は細かな調節ができるところにあるけど、平文 XML を配列に落とすだけなら、微調整もへったくれもない。



一点挙げるとすれば、getElementsByTagName はデフォルト空間の子孫ノードを全て拾うため、階層構造になったときに対応しきれなくなる。

> どのようなxml文書でも対応できる、根本的な理解と、それに基づいた方法を教えて頂けると良いなと考えていました。

XML の抽出には強力な XSL 及び XQuery があるため、抽出を自由に行いたいのであれば、そちらをお勧めします。

PHP では XSLT 1.0 しか利用できないけど、幸い、php:function を利用できるため、それほど不自由は感じない。

XML であることの利点は、階層構造は元より、環境を問わず(この利点は大きい)部分的な抽出、取り込み、関連付け、要素への重み付け等が自由に行えるところにある。

XML であることを放棄して、配列に落とすなら、PHP でロジックを組むことになるのだけれど、その辺は好きなように。

PHP だけで狭視的に利用するだけなら、CSV で十分だと思うんだけどね。必要に応じてデータベースに落とせるし。
    • good
    • 0
この回答へのお礼

回答ありがとうございます。

>XPath の利点は細かな調節ができるところ

そうですね、利点はそのようなところにあるのだと思っていました。
ただ、XPathのコード記述は見た目がゴチャゴチャしていて、
個人的には好きではないんですよね。
あと、処理速度は遅くならないのかなぁ、という気もしています。

>一点挙げるとすれば、getElementsByTagName はデフォルト空間の子孫ノードを全て拾うため、階層構造になったときに対応しきれなくなる。

ここ、そうでした、ここです。
この点で、「こんなことはできないんですか?」

という疑問がありまして、
つまり、

getElementsByTagNameをルート要素以外に対しても使いたいのですが、
そんなことはできないんですよね?
というか、それと似たようなことを可能にしたのが、XPathなのでしょうか。

getElementsByTagName('price')
というのを、<root>に対してメソッド実行すると、
各<item>の中にある<price>が
全てが対象になりますが、これを、
ある1つの<item>に対してメソッド実行すると、
その<item>の中にある<price>1つが対象となる、
みたいな感じで使えたらいいなと思っているわけです。

分かりにくかったら、すみません。

お礼日時:2011/01/19 01:20

$ary = array(


'id'=> 'number'
,'name'=> 'string'
,'price'=> 'number'
,'color'=> 'string'
,'size'=> 'number'
,'option'=> 'string'
);

$result = array();

$doc = new DOMDocument;
$doc->loadXML($xml);

$xpath = new DOMXPath($doc);

foreach ($xpath->query('/child::root/child::items/child::item') as $item) {
$params = array();
foreach ($ary as $name => $type) {
$params[$name] = $xpath->evaluate("{$type}(child::{$name}/child::text())", $item);
}
$result[] = $params;
}
var_dump($result);

配列に置き換えるくらいなら、正直 simple_xml のが楽。
XML の構造が単純だし、文字列変換して eval したい気分にもなる。
    • good
    • 0
この回答へのお礼

回答ありがとうございます。
DOMXPathを用いた方法を教えて頂き、大変参考になりました。
私に知識がないために、
本件において、DOMXPath を使う意義が分からなかったのですが、
もし何らかの意図なり目的があるようでしたら、教えてください。

最善かどうかは別として、
「他にも、こんな方法がありますよ」
という意味で、DOMXPathによる方法を教えて頂いたのでしょうか。

>配列に置き換えるくらいなら、正直 simple_xml のが楽。

とありましたから、
「回答NO.1の方法で良いが、他に方法はないかと問われれば、
DOMXPathによる方法を書きますが。」
といったお気持ちだったのでしょうか。
(失礼な書き方をしていたとしたら、すみません!)

もし宜しければ、教えて下さい。

お礼日時:2011/01/18 23:18

simplexml_load_file を使います。



-- テストソース
$xml = simplexml_load_file('sample.xml');
print_r($xml);
print $xml->items->item[0]->option;
print isset($xml->items->item[0]->option)? "true":"false";
print isset($xml->items->item[1]->option)? "true":"false";

-- 出力結果(カッコなど整形してます)
[items] => SimpleXMLElement Object
 [item] => Array
  [0] => SimpleXMLElement Object
   [id] => 1
   [name] => 商品(赤)
   [price] => 1500
   [color] => red
   [size] => 8
   [option] => A
  [1] => SimpleXMLElement Object
   [id] => 2
   [name] => 商品(青)
   [price] => 700
   [color] => blue
   [size] => 3
  [2] => SimpleXMLElement Object
   [id] => 3
   [name] => 商品(緑)
   [price] => 1200
   [color] => green
   [size] => 5
   [option] => C
A
true
false
    • good
    • 0
この回答へのお礼

大変わかりやすい回答をありがとうございます。

>simplexml_load_file を使います。

こちらの方法について、よく分かりました。
ありがとうございます。

childNodesを用いた方法による回答を想定していましたが、
教えて頂いた方法でも、私の希望を叶えてくれますね。
とても勉強になりました。
ありがとうございます。

一方で、
simplexmlを使わない方法でも、
何か方法がないか知りたいと思っています。

もしどなたかご存知でしたら、教えて下さい。

お礼日時:2011/01/17 18:28

お探しのQ&Aが見つからない時は、教えて!gooで質問しましょう!