PHP SimpleXML関数でWXR形式のxmlファイルをパースする

PHP, WordPress                

  公開日:2020年10月22日  最終更新日:2020年11月5日

WXR形式のxmlファイル」とはWordPressのエクスポート機能により出力されたファイルの事です。
出力されたxmlファイルの中には、基本的に全ての投稿、全てのカテゴリーとタグ、投稿とカテゴリー、タグの関連付け、投稿毎のカスタムフィールドが含まれます。

今回はこのWXR形式のxmlファイルを別のCMSへインポートする為のコンバーターを作成する中でSimpleXML関数が一筋縄では行かなかったので、その理由と対処法です。

WXR形式のxmlファイルの内容

WXR形式のxmlファイルの中はだいたいこんな感じのフォーマットになっています。(これはpostデータの部分)今回はこれをパースします。

<item>
		<title>
				ここにタイトル		</title>
		<link>http://example.com/20201022/</link>
		<pubDate>Fri, 22 May 2015 09:33:13 +0000</pubDate>
		<dc:creator><![CDATA[nishioka]]></dc:creator>
		<guid isPermaLink="false">https://example.com/wp/?p=1681</guid>
		<description></description>
		<content:encoded>
				<![CDATA[
ここにコンテンツ本文が入っています。
]]>		</content:encoded>
		<excerpt:encoded>
				<![CDATA[
ここには抜粋が入っています。
]]>		</excerpt:encoded>
		<wp:post_id>1681</wp:post_id>
		<wp:post_date><![CDATA[2015-05-22 18:33:13]]></wp:post_date>
		<wp:post_date_gmt><![CDATA[2015-05-22 09:33:13]]></wp:post_date_gmt>
		<wp:comment_status><![CDATA[closed]]></wp:comment_status>
		<wp:ping_status><![CDATA[closed]]></wp:ping_status>
		<wp:post_name><![CDATA[mov20140717]]></wp:post_name>
		<wp:status><![CDATA[publish]]></wp:status>
		<wp:post_parent>0</wp:post_parent>
		<wp:menu_order>0</wp:menu_order>
		<wp:post_type><![CDATA[post]]></wp:post_type>
		<wp:post_password><![CDATA[]]></wp:post_password>
		<wp:is_sticky>0</wp:is_sticky>
		<category domain="category" nicename="movie"><![CDATA[MOVIE]]></category>
		<wp:postmeta>
		<wp:meta_key><![CDATA[_edit_last]]></wp:meta_key>
		<wp:meta_value><![CDATA[1]]></wp:meta_value>
		</wp:postmeta>
		<wp:postmeta>
		<wp:meta_key><![CDATA[_wp_old_slug]]></wp:meta_key>
		<wp:meta_value><![CDATA[xxxxxxxx]]></wp:meta_value>
		</wp:postmeta>
		<wp:postmeta>
		<wp:meta_key><![CDATA[_wp_old_slug]]></wp:meta_key>
		<wp:meta_value><![CDATA[zzzzzzzz]]></wp:meta_value>
		</wp:postmeta>
		<wp:postmeta>
		<wp:meta_key><![CDATA[_thumbnail_id]]></wp:meta_key>
		<wp:meta_value><![CDATA[1684]]></wp:meta_value>
		</wp:postmeta>
</item>

SimpleXML関数について

SimpleXML関数でxmlフォーマットの文章をパースする場合、次の2つの関数どちらかを使うことになると思います。

simplexml_load_file() //ファイルから直接
simplexml_load_string() //文字列としてのxmlから

今回はsimplexml_load_string関数の方を使っています。

実装

早速実装してみます。

$xmlstr = file_get_contents($import_file_path);
$parse_data = simplexml_load_string($xmlstr ,'SimpleXMLElement', LIBXML_NOCDATA);

こんな感じで、まずはfile_get_contentsでxml文書を読み込み、続けさま、simplexml_load_stringでパースします。(file_get_contentsを使った理由は後ほど)

あとは、パースされたデータをループぶん回してそれぞれの値をいい感じに加工して、別CMSへさっと取り込み完了!!と思いきや

Warning: simplexml_load_string() [function.simplexml-load-string]: Entity: line 1: parser error : attributes construct error in...

こんな感じで、simplexml_load_string呼び出し直後にエラーとなりパース自体ができません。

この原因はsimplexml_load_stringがxml文書中に含まれる制御文字をいい感に扱ってくれないからです。

このエラーの解消は、原因が制御文字なので不要な制御文字をパース前に削除すれば大丈夫。
以下のコードは「改行(LF,CP)」以外の制御文字を半角スペースに置き換えます。

$xmlstr = preg_replace('/[\x00-\x09\x0B\x0C\x0E-\x1F\x7F]/',' ',$xmlstr); //改行は残す

WXR形式のxmlファイルをパースする際、改行まで削除するとコンテンツ部分の改行も削除され、コンテンツの体裁が崩れますのでご注意を。

今回、事前にfile_get_contentsでファイルの内容を読み込んでからsimplexml_load_stringでパースした理由はこの対処をする為です。

気を取り直してもう一度実行。
今度はちゃんとエラーなくパースされたようですが、思っていたデータが取れていませんでした。

「タイトル」を取得したい場合は以下のコードで取得できます

// $itemはループ中に親ノードから取得
$item->title;

では「コンテンツ(本文)」を取得したい場合

// NGコード
$item->content;

上記コードでは取得できません。
そう!ネームスペース(名前空間)が定義されたノードの値はそのままで取得できないんです。

ネームスペースが定義されたノード値の取得

こちらの記事ではgetNamespacesメソッドを利用して、一旦名前空間のデータを取得して対象ノードのデータを取得しています。https://liginc.co.jp/programmer/archives/5317

LIGのやり方でもいいですが、私の場合は以下のコードにて

// OKコード
// String型にキャストしない場合、Object型で返されます
(String)$item->children('content',true)->encoded;

childrenメソッドで名前空間名を指定するので、わざわざgetNamespacesを使う必要はないんじゃないかと思いました。

なんか、もうちょっとサクッと進んでくれると思ってて、名前空間に関しては仕方ない気もしますが、制御文字系はライブラリ内で正しくパースできるようにして欲しいですよね。