とあるサービス構築構築をしている時に、上場会社の財務情報を取得するために、XBRLというフォーマットを扱う必要があったのですが、色々な場面で使われているXBRLは中身はXMLとHTMLの混合の仕様になっており、これらをPHPで扱う時には、XML操作が必須になります。
xmlはタグや属性を自由に登録できるため、かなり汎用的なデータベースとしても利用が可能なため、結構古くからWEBでやり取りするデータとしては使われていて、PHPもsimpleXMLという内部モジュールで、内部データを扱うことが可能になっています。
WEB系エンジニアの人は、SQLとjsonが扱いやすいデータ構造だと思いますが、xmlもそんなに大差ないデータベースではあるため、苦手感を持たずにデータアクセスをしてみたところ、非常に初歩で躓きポイントがあったので、その対応方法も含めてブログに掲載しておきます。
下位検索が全検索になってしまう問題
一つのXMLから任意のグループ別に情報を取得してそれぞれのグループ毎にデータを抽出したくなる場合の時に、まずxpathで対象のグループ一覧を取得し、配列に保存し、forかforeachでそれぞれのグループ毎にさらにxpathを行う際に、何故かグループ内ではなく、全体から検索してしまうという、不具合に近い症状が発生します。
具体的なデータと内容と下記に載せておきます。
<?xml version="1.0" encoding="utf-8"?>
<aaa>
<bbb>
<ccc>001</ccc>
</bbb>
<bbb>
<ccc>002</ccc>
</bbb>
</aaa>
<?php
$dom = new \DOMDocument();
$dom -> load("sample.xml");
$xml = simplexml_import_dom($dom);
$arr = $xml->xpath("//bbb");
foreach($arr as $node){
print_r($node->xpath("//ccc"));
}
Array
(
[0] => SimpleXMLElement Object
(
[0] => 001
)
[1] => SimpleXMLElement Object
(
[0] => 002
)
)
Array
(
[0] => SimpleXMLElement Object
(
[0] => 001
)
[1] => SimpleXMLElement Object
(
[0] => 002
)
)
「bbb > ccc」という値をそれぞれ取得しようとしたわけですが、思った通りにxpathを書いて見たところ、2つとも同じ結果が表示されてしまいました。
ちなみに、xpathの中に記述している「//***」は、内部検索をするというような仕様であり、bbbタグを取得して2つの配列が生成され、それを$nodeに入れ込んだforeachを実行しているのですが、「$node->xpath("//ccc")」が$nodeの上位階層から検索されているというのが、普通に考えるとなかなか癖の強い関数だと思われます。
xpath操作で四苦八苦
ダイレクトアクセス
<?php
$dom = new \DOMDocument();
$dom -> load("sample.xml");
$xml = simplexml_import_dom($dom);
print_r($xml->xpath("/aaa/bbb[1]/ccc"));
print_r($xml->xpath("/aaa/bbb[2]/ccc"));
Array
(
[0] => SimpleXMLElement Object
(
[0] => 001
)
)
Array
(
[0] => SimpleXMLElement Object
(
[0] => 002
)
)
直接階層指定をしたところ、想定した結果を得ることができましたが、これでは、汎用的なデータ取得がやりずらいので、このやり方は却下なのですが、bbbを配列で指定しているところがミソなのですが、開始番号が1からという点が、プログラマーの人たちが眉をしかめるポイントではないでしょうか?
awk言語の配列も確か1スタートだったので、「これはコレ!」と思って取り組むしかないでしょう。
検索指定ではないタグ指定
<?php
$dom = new \DOMDocument();
$dom -> load("sample.xml");
$xml = simplexml_import_dom($dom);
$arr = $xml->xpath("//bbb");
foreach($arr as $node){
print_r($node->xpath("ccc"));
}
Array
(
[0] => SimpleXMLElement Object
(
[0] => 001
)
)
Array
(
[0] => SimpleXMLElement Object
(
[0] => 002
)
)
今度も同じ結果になりましたが、これは、「$node->xpath("ccc")」という風に、「//***」という記述ではなく、タグのみを書いて見たところ、どうやら下位層を拾うことができるようです。
ただし、このやり方はcccが検索されたグループのrootになっていないといけないようです。
cccの下にdddが存在してそこにアクセスしたい場合は、「$node->xpath("ccc/ddd")」と書かなくては行けなくて、「$node->xpath("ddd")」という風に書くと、ブランクが返ってきてしまいます。
このやり方も階層が固定であればいいのですが、できれば、下位層検索になる方式で行いたいので、このやり方も却下です。
解決方法
このやり方を解決するには、root階層を切り替える必要があるので、以下のようなコードを書くことで想定の結果が得られました。
<?php
$dom = new \DOMDocument();
$dom -> load("sample2.xml");
$xml = simplexml_import_dom($dom);
$arr = $xml->xpath("//bbb");
foreach($arr as $node){
$node = simplexml_load_string($node->asXML());
print_r($node->xpath("//ddd"));
}
Array
(
[0] => SimpleXMLElement Object
(
[0] => 001
)
)
Array
(
[0] => SimpleXMLElement Object
(
[0] => 002
)
)
ちゃんと、グループ階層の下位層が検索されています。
rootにcccが存在しなくても検索しに行ってくれます。
簡単解説
ポイントは「$node = simplexml_load_string($node->asXML());」の行で、$nodeを一度「asXML()」でxml文字列に切り替えてそれを「simplexml_load_string()」でxmlオブジェクト化して上書きしています。(別に上書きしなくてもいいのですが・・・)
こうすることで、オブジェクトがその階層しか存在しなくなるので、検索範囲が特定されるということなんですね。
なんじゃこりゃ!
普通にプログラミングしていて、「これはないやろ〜〜〜!」ていう感じでしたが、歯を食いしばって我慢しましょう。
それにしても、これってphpのxpathの症状なのでしょうか?ホントにバグにしか考えられないんですけどね。
どんな仕様やねん!!
0 件のコメント:
コメントを投稿