【PHP】APIで取得したXML形式レスポンスをオブジェクト操作する

2023.06.25

今回は国立国会図書館が提供するAPI(NDL Search)を使ってレスポンステストを行ってみた。

XML形式でのAPI連携を行ったこともあり、前回行ったJSON形式のAPIと若干操作方法が異なるので、忘れないうちにメモとしてやり方を残しておく。

【PHP】OpenWeatherMapでAPI接続テストをしたやり方と記録

API接続先のURLと値を定義

まずはAPI提供元に送るリクエストを書いていく。

今回は「1週間でPHPの基礎が学べる本」という私がお世話になった参考書を検索した結果が返ってく前提で作ってみる。

$bookTitle = "1週間でPHPの基礎が学べる本";//本のタイトルを明記
$apiUrl = "https://iss.ndl.go.jp/api/sru?operation=searchRetrieve&query=title%3D%22" . urlencode($bookTitle) . "%22&recordSchema=dcndl_simple";//国立国会図書館APIを検索するためのURLを定義

URL部分の細かな解説は下記の通りだ。

  • ①SRU(Search/Retrieve via URL)検索プロトコルに準拠
  • ②operation=searchRetrieve: このパラメータは、APIのどの操作を実行するかを指定
    →%3D%22 はURLエンコードされた文字列で、%3D は等号(=)を表し、%22 は二重引用符(”)を表す
  • ③query=はAPIに送るクエリを定義
  • ④ecordSchema=dcndl_simpleは国立国会図書館で用いられるシンプルなメタデータスキーマ(APIスキーマー=APIから返されるデータの構造や形式を定義するためのもの)

API提供元に送る情報はそろったので、今度はさらに細かなオプションを設定していく。

cURLでAPIリクエスト

リクエスト送信には、cURLというPHPのライブラリを使ってAPIリクエスト(新しいcURLセッション)を初期化して行う。

$curl = curl_init();

この関数を使うときは、通常APIリクエストの最初のステップとして呼び出すので、上記はお作法として覚えておこう。

オプションを設定

ここからはリクエスト時のオプションを設定していく。
下記のオプションを設定することで、APIリクエストの動作を詳細に制御することができる。

curl_setopt_array($curl, array(//cURLライブラリに含まれる関数で、一度に複数のcURLオプションを設定するために使用(設定したいオプションとその値をペアとした連想配列にする)
    CURLOPT_URL => $apiUrl,//リクエストの結果を文字列として表示
    CURLOPT_RETURNTRANSFER => true,//リクエスト結果から得る戻り値を文字列として受け取る。falseの場合は実行結果が文字列としてではなく、元のデータとして表示される
    CURLOPT_TIMEOUT => 30,//30秒以上の通信は行われない(30秒以上かかる場合はcURLはエラーを返す)
    CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,//通信にHTTPプロトコルが必要ない場合は必要なし(仕様書によるので、必要に応じて更新)
    CURLOPT_CUSTOMREQUEST => "GET",//cURLリクエストでこちらから送るデータをGETにしている。URLのデフォルトのHTTPメソッドはGETなので、記入する必要はないが、今回は明示的に書いてるだけ(POSTでもOKで、案件によって使い分ける)
    CURLOPT_HTTPHEADER => array(//HTTPヘッダー(本体データに関する各種のメタデータが含まれる)を取得するもの
        "cache-control: no-cache"//CURLOPT_HTTPHEADER の中にcache-control: no-cacheを書けば、キャッシュがあっても常にヘッダーとボディの最新情報を取得できる。
        //※ヘッダーには、リクエストやレスポンス、またはその本体に関する情報(メタデータ)が含まれ、ボディには実際のコンテンツ(データ)が含まれる
    ),
));

いつも通り、コメントアウトにて詳細を解説しているので、そちらを参考にしてもらいたい。

cURL実行→終了時のお作法

上記の設定完了したら、cURLを実行し、API通信を行う。

$response = curl_exec($curl);//実際にAPIリクエストを実行し、レスポンスを受け取る
$err = curl_error($curl);//もしエラーがあればそれも取得
curl_close($curl);//cURLセッションを閉じる(お作法)

終了時には「curl_close()」を使って終了させる必要があるので、お作法として覚えておこう。

もしもレスポンスにエラーが起きたら

上記ではcurl_error関数を使って、エラーがあれば取得するという設定を行っているので、それを検証のために表示させる伏線も張っておく。

if (!$response) {//レスポンスが空の場合
    die("Empty response");//エラーメッセージを表示して処理を停止
}

エラーが無ければここは無視されるので、予防線程度に考えておけばオッケーだ。

レスポンス結果を表示してみる

通信が適切に行われ、レスポンスが帰ってきたかを調べるため、下記を書いてみる

//※確認後削除
header("Content-Type: text/plain");//HTTPレスポンスのヘッダーを設定。
echo $response;//レスポンスの本文表示
exit;
//※確認後削除

header関数では、HTTPレスポンスのヘッダーを設定している。

これはレスポンスの本文がプレーンテキスト(テキストの形式化されていない単純なテキスト)であることをクライアント示すためであり、ブラウザはこれに従ってコンテキストを解釈する。

そして取得したレスポンス(戻り値)は$responseに格納しているので、echoで表示している。

するとXML形式のコンテキストが表示されるはずだ。

※この部分はあくまでも検証用なので、確認後はコメントアウトするか、今までのコードと一緒に他のファイルに移植しておこう。

レスポンス結果をPHPのオブジェクトに変更(パース)

そしてエラーが出ることなく無事にレスポンス(戻り値)を受け取った場合の処理に入る。

ただ受け取ってもXMLファイルの中身が全て表示されてしまうので、これをPHPのオブジェクトに入れて、必要な情報だけ抜き取る準備をしていく(これをパースという)。

libxml_use_internal_errors(true);//libxmlは、XML関連の処理を行うためのライブラリ。trueを渡すと、libxmlが発生するエラーを内部に蓄えるようになる(すぐにエラーメッセージは出さない。)
$xml = new SimpleXMLElement($response);//SimpleXMLElementとはPHPに予め備えられているクラスであり、XMLに含まれている文字列(本文)をオブジェクトとして格納できる
//特定の形式のデータ(この場合はXML)を読み取り、そのデータをプログラムが扱える形式(上記)のことをパースと呼ぶ
if ($xml === false) {//XMLの本文をオブジェクトにする際にエラーがあったら
    echo "Failed loading XML: ";
    foreach(libxml_get_errors() as $error) {//new SimpleXMLElement($response); が失敗した場合、libxmlがエラーメッセージを内部に蓄えて、エラーが複数でも全て表示する
        echo "<br>", $error->message;
    }
    exit;
}

注目してほしいのは new SimpleXMLElement($response);にてクラスのインスタンスが行われていることだ。

ここは少しややこしいが「SimpleXMLElement」はPHPにデフォルトで備えられているクラスであり、XMLに含まれている文字列(本文)をオブジェクトとして格納できる機能をもっている。

つまりクラスは既に定義されていると思っておいてオッケーだ。

処理のフローは下記の通りだ。

  • ①libxmlライブラリを使って発生するエラーを内部に蓄えておく
  • ②PHPデフォルトのSimpleXMLElementの引数にレスポンス結果($response))をセット
  • ③XMLに含まれている文字列(本文)をオブジェクトとして格納
  • ④エラーがあれば①で蓄えていたエラーを全て表示する

パースした内容を操作して情報を取り出す

XMLとして受け取った内容をPHPのオブジェクト形式に変換(パース)できたので、ここからは必要な情報を抜き取っていく。

今回抜き取る情報は、本の名前、作為者名、出版社、発行日だ。

この部分は最初は非常にややこしいので、コメントアウトを見ながら進めてほしい。

foreach ($xml->records->record as $record) {//パースしたXML($xml)から、それぞれのレコードの情報(タイトル、著者、出版社、発行日、識別子)を取得し、それらを画面に出力
    //上記は連想配列ではなくSimpleXMLElementを使ってXMLのデータを扱う際の形式(オブジェクト操作という認識でオッケー)
    //$xml(パースしたXML全体)からrecords(エレメント)を取得し、その中のrecord(子エレメント)を取得。エレメントはAPI毎に異なるので、取得したXMLデータ(library_check.php)や仕様書を参照
    $sample = $record->recordData->children('http://ndl.go.jp/dcndl/dcndl_simple/')->dc;
    //複数あるrecordDataの中に「http://ndl.go.jp/dcndl/dcndl_simple/」がいくつか入っていると、衝突を起こすので、それを一意のものとして判断することができる(XMLの「xmlns」の部分で名前空間として既に設定されている)
    //→「http://ndl.go.jp/dcndl/dcndl_simple/」は一意であるため、複数ある場合それぞれに処理が走る
    //※名前空間を明示することで、具体的な「コンテキスト」または「カテゴリ」を指定して、同名の要素があっても混乱を避けることができる
    //つまりコンテキストの内容が全く同じでも、この名前空間の値によって、別の要素と判定することができるということ。
    //children()はSimpleXMLElementクラスのメソッドで、指定した上記の名前空間の子エレメントを返す
    $title = (string)$sample->children('http://purl.org/dc/elements/1.1/')->title;
    //上記はhttp://ndl.go.jp/dcndl/dcndl_simple/という名前空間をもつdcエレメントの、さらにhttp://purl.org/dc/elements/1.1/という名前空間をもつtaitleエレメントを指定しているということ
    $creator = (string)$sample->children('http://purl.org/dc/elements/1.1/')->creator;
    $publisher= (string)$sample->children('http://purl.org/dc/elements/1.1/')->publisher;
    $issued = (string)$sample->children('http://purl.org/dc/terms/')->issued;
    //上記は別の名前空間が使われている。dcterms(発行日の直前)とxmlns:dcterms(名前空間の直前)が一致しているか否かで入力する文字列を判断すればオッケー
    $identifier = (string)$sample->children('http://purl.org/dc/elements/1.1/')->identifier;
    echo "Title: {$title}<br>";
    echo "Author: {$creator}<br>";
    echo "publisher: {$publisher}<br>";
    echo "issued: {$issued}<br>";
    echo "identifier: {$identifier}<br>";
}

注意したいのは、foreach ($xml->records->record as $record)の部分だ。

これは連想配列を操作しているように見えるが、実はそうではなく、SimpleXMLElementを使ってXMLのデータを扱う際の形式となるので、連想配列の操作というよりはオブジェクトの操作という認識でオッケーだ。

行っている内容としては、$xml(パースしたXML全体)からrecords(エレメント)を取得し、その中のrecord(子エレメント)を取得している。

※エレメントはAPI毎に異なるので、先ほど検証時に取得したXMLデータ(library_check.php)や仕様書を参照して確認しよう。

キーを名前空間まで辿る

上記のコードで、最も重要になる要素が「名前空間」という要素だ。

$sample = $record->recordData->children('http://ndl.go.jp/dcndl/dcndl_simple/')->dc;

行っている内容としては下記の通りだ。

  • ①XML内にある「recordエレメント」
  • ②の中にある「recordDataエレメント」
  • ③の中にある「http://ndl.go.jp/dcndl/dcndl_simple/」文字列を子要素に持つ「dcエレメント」
  • ⑤を$sample変数として格納

このURL文字列の部分は名前空間と呼ばれている。

ざっくり説明すると、同じ要素が複数あったとしても、この名前空間の設定をしておくことで、それらの要素を一意として認識できるといったものだ。

この部分は子エレメントにて適応されているので、後ほど紹介する。

名前空間とは?

より分かりやすく解説するために、先ほど検証時にecho $response;で取得したXMLデータを見てもらうと、「xmlns:dcndl_simple=”http://ndl.go.jp/dcndl/dcndl_simple/”」という名前空間が設定されていることがわかる。

    <record>
      <recordSchema>info:srw/schema/1/dc-v1.1</recordSchema>
      <recordPacking>string</recordPacking>
      <recordData>
        <dcndl_simple:dc xmlns:dcndl_simple="http://ndl.go.jp/dcndl/dcndl_simple/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcndl="http://ndl.go.jp/dcndl/terms/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" xmlns:foaf="http://xmlns.com/foaf/0.1/" xmlns:owl="http://www.w3.org/2002/07/owl#">
          <dc:identifier xsi:type="dcterms:URI">https://iss.ndl.go.jp/books/R100000096-I012735940-00</dc:identifier>
          <dc:title>1週間でPHPの基礎が学べる本</dc:title>
          <dcndl:titleTranscription>1シュウカン デ PHP ノ キソ ガ マナベル ホン</dcndl:titleTranscription>
          <dc:creator>亀田健司著</dc:creator>
          <dc:creator>亀田, 健司</dc:creator>
          <dcndl:creatorTranscription>カメダ, ケンジ</dcndl:creatorTranscription>
          <dcterms:alternative>1週間でPHPの基礎が学べる本</dcterms:alternative>
          <dc:publisher>インプレス</dc:publisher>
          <dcterms:issued xsi:type="dcterms:W3CDTF">2022</dcterms:issued>
          <dcterms:extent>351p</dcterms:extent>
          <dcterms:extent>21cm</dcterms:extent>
          <dc:identifier xsi:type="dcndl:ISBN">9784295013570</dc:identifier>
          <dc:identifier xsi:type="dcndl:NIIBibID">BC13355845</dc:identifier>
          <dc:subject xsi:type="dcndl:NDLC">M159</dc:subject>
          <dc:subject xsi:type="dcndl:NDC10">007.645</dc:subject>
          <dc:subject xsi:type="dcndl:NDC10">007.64</dc:subject>
          <dc:subject xsi:type="dcndl:NDC9">547.4833</dc:subject>
          <dcndl:materialType>図書</dcndl:materialType>
          <dcndl:materialType>図書</dcndl:materialType>
          <dcndl:materialType>図書</dcndl:materialType>
          <rdfs:seeAlso rdf:resource="https://ci.nii.ac.jp/ncid/BC13355845"/>
          <dc:subject>ウェブアプリケーション</dc:subject>
          <dc:subject>ウェブアプリケーション</dc:subject>
          <dc:subject>プログラミング(コンピュータ)</dc:subject>
          <dc:language xsi:type="ISO639-2">jpn</dc:language>
        </dcndl_simple:dc>
      </recordData>
      <recordPosition>1</recordPosition>
    </record>

上記とほぼ同じ内容のコードが続くとしよう。

そのまったく同じコードを一意のものとして識別する方法として「xmlns:dcndl_simple=”http://ndl.go.jp/dcndl/dcndl_simple/”」のように値を設定している。なので、この部分を変えるだけで「dcndl_simple:dc」タグ内の要素が一意のものとして扱われる。

先ほどの「dcエレメント」とは<dcndl_simple:dc ~~>の子要素のことで、それらすべてにこの名前空間を持たせることができる。

複数あるrecordDataの中に「http://ndl.go.jp/dcndl/dcndl_simple/」がいくつか入っていると、衝突を起こすので、これにより全く同じコードを一意のものとして判断することができるのだ(XMLの「xmlns」の部分で名前空間として既に設定されている)。

名前空間は子エレメントにも適応できる

上記では本の名前、作者名、発行日など全ての要素に「”http://ndl.go.jp/dcndl/dcndl_simple/”」という共通の名前空間が割り振られているが、さらにその子要素も名前空間で識別することが可能だ。

例えば下記の情報を選択したいとしよう。

<dc:title>1週間でPHPの基礎が学べる本</dc:title>
<dc:creator>亀田健司著</dc:creator>

作者名と本の名前はPHP側で下記の処理を行うことで取り出しが可能だ。

$title = (string)$sample->children('http://purl.org/dc/elements/1.1/')->title;

これは先ほど「”http://ndl.go.jp/dcndl/dcndl_simple/”」($dc)の共通の名前空間の子エレメントである「titleエレメント(’http://purl.org/dc/elements/1.1/’という名前空間を持っている)」を指定している。

つまり共通の名前空間(~/dcndl_simple/)を持つと同時に、子要素には別の名前空間(~1.1/)が割り振られているのだ。

この割り当てられた名前空間の調べ方としては「<dc:title>1週間でPHPの基礎が学べる本</dc:title>」のdcが「xmlns:dc=”http://purl.org/dc/elements/1.1/”」にて定義されている。

子要素の名前空間が異なる場合

XML内では発行日のコードが異なることに気付いただろうか。

<dc:title>1週間でPHPの基礎が学べる本</dc:title>
<dcterms:issued xsi:type="dcterms:W3CDTF">2022</dcterms:issued>

これは発行日にはdcの「~1.1/」という名前空間ではなく、dctermsという名前空間(xmlns:dcterms=”http://purl.org/dc/terms/” で定義)が割り当てられているからだ。

なので、これを表示するには下記のように名前空間の文字列をPHP側で指定する必要がある。

$issued = (string)$sample->children('http://purl.org/dc/terms/')->issued;

これも同じく、「”~dcndl_simple/”」($dc)の共通の名前空間の子エレメントである「issuedエレメント(”~terms/”という名前空間を持っている)」を指定している。

子要素を識別する

これにより、もし同じコンテキストのエレメントがあったとしても、下記のようにdcの部分を別の名前空間、例えばhttp://abcabcabc/がzzという名前で指定されていれば、別要素として操作できるのだ。

<dc:title>1週間でPHPの基礎が学べる本</dc:title>
<zz:title>1週間でPHPの基礎が学べる本</dc:title>

これは例として書いているが、「xmlns:zz=”http://~~~」のように名前空間が付けれれている部分を探せばオッケーだ。

取り出した内容を表示する

取り出したい内容を名前空間とともに指定したところで、最後に「echo “Title: {$title};」で表示すれば、無事に表示することができる。

注意としては、ループ処理でXML内の内容をある分だけ取得しているので、名前空間が被っているものも全て表示されることだけ覚えておこう。

後はこれまでのやり方に従って欲しい情報を適材適所で取得し、フロント部分を整えていけばオッケーだ。

PIC UP