[PHP] XML操作のxpathでのハマりどころ

2018年10月19日

PHP テクノロジー プログラミング

とあるサービス構築構築をしている時に、上場会社の財務情報を取得するために、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の症状なのでしょうか?ホントにバグにしか考えられないんですけどね。 どんな仕様やねん!!

人気の投稿

このブログを検索

ごあいさつ

このWebサイトは、独自思考で我が道を行くユゲタの少し尖った思考のTechブログです。 毎日興味がどんどん切り替わるので、テーマはマルチになっています。 もしかしたらアイデアに困っている人の助けになるかもしれません。

ブログ アーカイブ