久しぶりに PHP を使っていて気付いたことをブログネタにしました。
最近の Web API/REST API は JSON フォーマットで結果を取得できるものが多いですが、中には XML のみサポートされているものもあります(RSS とかね)。そんな XML を PHP で扱う方法について調べました。
まず、PHP には simplexml_load_file および simplexml_load_string という便利な関数が用意されています。これらは XML ファイル(のURL)や XML 文字列を指定して、PHP のオブジェクト化するというものです。例えば以下のような ATOM フィードフォーマットの XML ファイル(atom.xml)があったとします:
この atom.xml ファイルに対して、例えば以下のようなコード(test1.php)を用意して実行します:
その実行結果(赤字部分)はこちら:
これだけ。simplexml_load_file 関数や simplexml_load_string 関数は XML ファイルや XML 文字列を簡単に PHP のオブジェクト化して、XML 内の各データや属性値にアクセスできるようになるものです。
※上記のように属性値は $entry->link->attributes()->属性名 で取得できます。
さてここまでは普通にできるのですが、実はこの方法では取得が難しいデータがあります。各 entry 内にある <dc:updated> という特殊な名前のデータです。
この部分が少し厄介なのです。test1.php と全く同じ要領でこんな test2.php ファイルを作ってみます:
この test2.php を実行すると・・・ Parse error となってしまいます。PHP の文法ではここに ":" を記述してはいけないんですね。。
なるほど。これでは文法的にNGということはわかった。が、問題はどうやって回避すればいいのか、です。simplexml_load_file 関数を使わずに別の方法で XML をパースする方法もありますが、それはそれで面倒だったりします。なんとか simplexml_load_file の便利な機能をそのままに回避できないか・・
そこでググって見つけたがこの方法でした。簡単にいうと
(1) タグ(<から>まで)内の ":" を "_" とかに一括変換する
(2) ただしプロトコル( http:// とか https:// とか)で使われている部分だけは元に戻す
という方法です。この方法で作り直したのがこちら(test3.php)です:
青字部分ではまず普通に atom.xml ファイルを文字列として読み込み、上記 (1) および (2) の処理を行います。そしてその結果を simplexml_load_string 関数でオブジェクト化しました。このようにすると、各 entry 内の <dc:updated> 要素は <dc_updated> と変わっているので、そのまま扱えるようになります。
この test3.php を実行すると、期待通りの結果を得ることができました:
とりあえず動くことは動きました。でももうちょっと上手い方法がないもんかな。。
最近の Web API/REST API は JSON フォーマットで結果を取得できるものが多いですが、中には XML のみサポートされているものもあります(RSS とかね)。そんな XML を PHP で扱う方法について調べました。
まず、PHP には simplexml_load_file および simplexml_load_string という便利な関数が用意されています。これらは XML ファイル(のURL)や XML 文字列を指定して、PHP のオブジェクト化するというものです。例えば以下のような ATOM フィードフォーマットの XML ファイル(atom.xml)があったとします:
<?xml version="1.0" encoding="utf-8"?> <feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ja"> <id>tag:phpxml/</id> <title>サンプルタイトル</title> <link rel="alternate" type="text/html" href="http://sample.xml.jp/feed/"/> <link rel="self" type="application/atom+xml" href="http://sample.xml.jp/feed/atom10.xml"/> <entry> <id>http://sample.xml.jp/test1.html</id> <title>紹介タイトルその1</title> <link rel="alternate" type="text/html" href="http://sample.xml.jp/test1.html"/> <summary>紹介するエントリの1つです。</summary> <dc:updated>Thu, 26 Oct 2017 07:00:00 +0900</dc:updated> </entry> <entry> <id>http://sample.xml.jp/test2.html</id> <title>紹介タイトルその2</title> <link rel="alternate" type="text/html" href="http://sample.xml.jp/test2.html"/> <summary>紹介するエントリの2つめです。</summary> <dc:updated>Fri, 27 Oct 2017 07:00:00 +0900</dc:updated> </entry> </feed>
この atom.xml ファイルに対して、例えば以下のようなコード(test1.php)を用意して実行します:
<?php
$feed = simplexml_load_file( "atom.xml" ); echo "id: " . ( $feed->id ) . "\n"; echo "title: " . ( $feed->title ) . "\n";
foreach( $feed->entry as $entry ){ echo "entry_title: " . ( $entry->title ) . "\n"; echo "link_url: " . ( $entry->link->attributes()->href ) . "\n"; }
?>
その実行結果(赤字部分)はこちら:
$ php -f test1.php id: tag:phpxml/ title: サンプルタイトル entry_title: 紹介タイトルその1 link_url: http://sample.xml.jp/test1.html entry_title: 紹介タイトルその2 link_url: http://sample.xml.jp/test2.html
これだけ。simplexml_load_file 関数や simplexml_load_string 関数は XML ファイルや XML 文字列を簡単に PHP のオブジェクト化して、XML 内の各データや属性値にアクセスできるようになるものです。
※上記のように属性値は $entry->link->attributes()->属性名 で取得できます。
さてここまでは普通にできるのですが、実はこの方法では取得が難しいデータがあります。各 entry 内にある <dc:updated> という特殊な名前のデータです。
: : <title>紹介タイトルその1</title> <link rel="alternate" type="text/html" href="http://sample.xml.jp/test1.html"/> <summary>紹介するエントリの1つです。</summary> <dc:updated>Thu, 26 Oct 2017 07:00:00 +0900</dc:updated> : :
この部分が少し厄介なのです。test1.php と全く同じ要領でこんな test2.php ファイルを作ってみます:
<?php
$feed = simplexml_load_file( "atom.xml" ); echo "id: " . ( $feed->id ) . "\n"; echo "title: " . ( $feed->title ) . "\n";
foreach( $feed->entry as $entry ){ echo "entry_title: " . ( $entry->title ) . "\n"; echo "link_url: " . ( $entry->link->attributes()->href ) . "\n"; echo "dc:updated: " . ( $entry->dc:updated ) . "\n"; }
?>
この test2.php を実行すると・・・ Parse error となってしまいます。PHP の文法ではここに ":" を記述してはいけないんですね。。
$ php -f test2.php
Parse error: syntax error, unexpected ':' in test2.php on line 8
なるほど。これでは文法的にNGということはわかった。が、問題はどうやって回避すればいいのか、です。simplexml_load_file 関数を使わずに別の方法で XML をパースする方法もありますが、それはそれで面倒だったりします。なんとか simplexml_load_file の便利な機能をそのままに回避できないか・・
そこでググって見つけたがこの方法でした。簡単にいうと
(1) タグ(<から>まで)内の ":" を "_" とかに一括変換する
(2) ただしプロトコル( http:// とか https:// とか)で使われている部分だけは元に戻す
という方法です。この方法で作り直したのがこちら(test3.php)です:
<?php
//. XML ファイルを文字列変数へ読み込む
$txt = file_get_contents( "atom.xml" );
//. <***:****> -> <***_****>
$txt = preg_replace( "/<([^>]+?):(.+?)>/", "<$1_$2>", $txt );
//. プロトコルは元に戻す
$txt = preg_replace( "/_\/\//", "://", $txt );
$feed = simplexml_load_string( $txt );
echo "id: " . ( $feed->id ) . "\n";
echo "title: " . ( $feed->title ) . "\n";
foreach( $feed->entry as $entry ){
echo "entry_title: " . ( $entry->title ) . "\n";
echo "link_url: " . ( $entry->link->attributes()->href ) . "\n";
echo "dc:updated: " . ( $entry->dc_updated ) . "\n";
}
?>
青字部分ではまず普通に atom.xml ファイルを文字列として読み込み、上記 (1) および (2) の処理を行います。そしてその結果を simplexml_load_string 関数でオブジェクト化しました。このようにすると、各 entry 内の <dc:updated> 要素は <dc_updated> と変わっているので、そのまま扱えるようになります。
この test3.php を実行すると、期待通りの結果を得ることができました:
$ php -f test3.php
id: tag:phpxml/
title: サンプルタイトル
entry_title: 紹介タイトルその1
link_url: http://sample.xml.jp/test1.html
dc:updated: Thu, 26 Oct 2017 07:00:00 +0900
entry_title: 紹介タイトルその2
link_url: http://sample.xml.jp/test2.html
dc:updated: Fri, 27 Oct 2017 07:00:00 +0900
とりあえず動くことは動きました。でももうちょっと上手い方法がないもんかな。。