PHP - 複数の API を呼び出そう!
今日では、Google, Yahoo, Amazon などいろいろなところで情報をAPI(Application Program Interface)というかたちで提供しています。そこで、それらの情報を PHPによって収集/利用するのに cURLというプロジェクトを利用してみます。
cURLとは
cURLとは、いろいろな通信プロトコルを用いてデータを転送するライブラリやコマンドを提供するプロジェクトで、Client for URLsからきています。
この cURLを利用し、いろいろなサービスが提供している情報を収集することができます。
cURLを使ってみる
実際に Yahoo JAPANが提供している RSSを取得してみよう。(エラー処理は省いています)
<?php $sUrl = "http://headlines.yahoo.co.jp/rss/eiga_c_ent.xml"; $conn = curl_init(); curl_setopt($conn, CURLOPT_URL, $sUrl); curl_setopt($conn, CURLOPT_RETURNTRANSFER, TRUE); curl_setopt($conn, CURLOPT_TIMEOUT, 5); curl_setopt($conn, CURLOPT_HEADER, FALSE); $ret = curl_exec($conn); curl_close($conn); $xml = simplexml_load_string($ret); foreach($xml->channel->item as $item) { echo (string)$item->title."\n"; } ?>
このように簡単に取得する事が出来ます。しかし、API を複数呼んで取得した情報をまとめて利用する場合、API ひとつひとつを順番に呼んで処理していては最後の情報を取得するまでに無駄な時間がかかってしまいます。そこで、cURL multi という技術を利用すると APIを同時に複数呼ぶことが出来ます。実際の通信時間は一番遅い APIとなります。
※ 同時に APIを呼ぶか、順番に API を呼ぶかは、情報の利用方法によって異なります。
cURL multi を利用するなら自分なりのクラスを作ってみる
クラスを作る上で以下の点に注意してみます。
cUrl multiによって複数の APIを並列にコールする
各APIが返す結果(XML, JSON, etc)を格納する
現時点では HTTP/HTTPS のみ対応
APIを何度の呼ぶのは通信コストが高いのでキャッシュの機能を組み込む
キャッシュデータを小さくする為に圧縮可能にする
POST呼び出しによる APIの結果はキャッシュしない
キャッシュの仕組みはいろいろあるが、最も簡単な APC(Alternative PHP Cache)を利用してみる。この部分は、memcachedなど別のキャッシュシステムに変更出来るよう考慮しておく。
以下が作ってみたクラス:
<?php
/**
* QueryRequestクラス
*
* @package Gadgety
* @create 2007.12.05 Shinta
*
* @version $Id$
*/
/**
* QueryRequestクラス
*
* <pre>
* 必要な extension: curl, md5, apc
* APCの設定例:
* extension = "apc.so"
* apc.enabled=1 # APCの有効(1)/無効(0)
* apc.shm_segments=1 # セグメント数
* apc.shm_size=64 # 共有メモリ・セグメントサイズ(MB)
* apc.num_files_hit=1024
* apc.gc_ttl=3600 # キャッシュエントリーが GC listに残るかもしれない時間
* apc.ttl=0 # 他のエントリーに割り当てられるまでキャッシュに残る時間。0は解放しない
* apc.mmap_file_mask=/tmp/apc.XXXXXX
* apc.filters= # キャッシュしないファイルリスト(カンマ区切り、正規表現可)
*
* ・cUrl multiによって複数の APIを並列にコールする
* ・各APIが返す結果(XML, JSON, etc)を格納
* ・現時点では HTTP/HTTPS のみ対応
* ・APIを何度の呼ぶのは通信コストが高いのでキャッシュの機能を組み込む
* ・キャッシュデータを小さくする為に圧縮可能にする
* ・POST呼び出しによる APIの結果はキャッシュしない
*
* << 利用方法 >>
* $objReq = new QueryRequest();
*
* // キャッシュの設定
* $objReq->enableCache();
* $objReq->enableCompressCache();
*
* // URLによる APIサーバ指定
* $sUrl = "http://api1.example.com/rss.php?key=main&p=%B2%F7%C5%AC";
* $objReq->addRequest("MainApi", $sUrl);
*
* // HASHによる APIサーバ指定
* $aryInfo = array(
* "HOST" => "api2.example.com" // 必須
* ,"URI" => "prog.cgi" // デフォルト "/"
* ,"TIMEOUT" => 8 // デフォルト 5秒
* ,"PORT" => 80 // デフォルト 80
* ,"PROTOCOL" => "http" // デフォルト "http"
* ,"QUERY" => array("key"=>"sub", "p"=>"快適") // デフォルト array()
* );
* $objReq->addHashRequest("SubApi", $aryInfo, 5);
*
* // リクエスト発行
* $objReq->execute();
*
* // 取得データ
* if ($objReq->getHttpStatus("MainApi") == 200) {
* // 取得データ(XMLなど)を解析利用
* $sMain = $objReq->getData("MainApi");
* if ( preg_match("/<\?xml.*\?>/", $sMain) ) {
* $rss = simplexml_load_string($sMain);
* foreach($xml->channel->item as $item) {
* echo (string)$item->title."\n";
* }
* }
* } else {
* // メイン情報が取れないから リロードページ?404?
* $sMain = $objReq->getData("MainApi");
* error_log($sMain);
* include("reload.inc");
* exit(0);
* }
*
* if ($objReq->getHttpStatus("SubApi") == 200) {
* // 処理
* $sSub = $objReq->getData("SubApi");
* ...
* } else {
* // 補足情報が取れないぐらいなら無視するなど・・・
* }
* </pre>
*
* @subpackage QueryRequest
* @access public
* @auther Shinta
*/
class QueryRequest {
/**@+
* 定数
*/
const DEF_PROTOCOL = "http"; // プロトコル
const DEF_PORT = "80"; // ポート
const DEF_TIMEOUT = 5; // タイムアウト(秒)
/**@-*/
/**
* リクエスト値
*
* @access private
* @var array
*/
private $aryReqList = array();
/**
* レスポンス値
*
* @access private
* @array
*/
private $aryResList = array();
/**
* リクエストデータ
*
* @access private
* @var array
*/
private $aryReqData = array();
/**
* キャッシュデータ
*
* @access private
* @var string
*/
private $sCache = null;
/**
* キャッシュ利用有無
*
* @access private
* @var boolean
*/
private $bCache = false;
/**
* キャッシュ圧縮モード
*
* @access private
* @var boolean
*/
private $bGzCache = false;
/**
* インフォメーションログの表示有無
*
* @access private
* @var boolean
*/
private $bShowInfo = false;
/**
* コンストラクタ
*
* @access public
* @return void
*/
function __construct() {
$this->init();
}
/**
* デストラクタ
*
* @access public
* @return void
*/
function __destruct() {
$this->sCache = null;
}
/**
* リクエスト/受信データの初期化
*
* @access public
* @return void
*/
function init() {
$this->aryReqList = array();
$this->aryResList = array();
$this->aryReqData = array();
}
/**
* APIコール設定メソッド(HASH版)
*
* @access public
* @param string $sLabel API識別ラベル
* @param array $aryInfo API情報
* @param int $iCache キャッシュの有効時間
* @return boolean
*/
function addRequestHash($sLabel, $aryInfo, $iCache=0) {
if ( empty($sLabel) || !is_array($aryInfo) || count($aryInfo) == 0 ) {
$this->error("request entry error: label or url is empty");
return false;
}
// URL生成
if ( isset($aryInfo['HOST']) && $aryInfo['HOST'] != '' ) {
$sPro = self::DEF_PROTOCOL;
if ( isset($aryInfo['PROTOCOL']) ) {
$sPro = $aryInfo['PROTOCOL'];
}
$sPro .= "://";
$sUrl = "/";
if ( isset($aryInfo['URI']) && $aryInfo['URI'] != '' ) {
if ( substr($aryInfo['URI'], 0, 1) == "/" ) {
$sUrl = $aryInfo['URI']; // 初期化
} else {
$sUrl .= $aryInfo['URI']; // 追加
}
}
$sQuery = "";
if ( isset($aryInfo['QUERY']) && is_array($aryInfo['QUERY']) ) {
foreach( $aryInfo['QUERY'] as $sKey => $sVal ) {
if ( $sQuery == "" ) {
$sQuery .= '?';
} else {
$sQuery .= '&';
}
$sQuery .= $sKey."=".rawurlencode($sVal);
}
}
$this->aryReqList[$sLabel]['URL']
= $sPro.$aryInfo['HOST'].$sUrl.$sQuery;
$this->aryReqList[$sLabel]['URL_MD5']
= $this->makeCacheKey($this->aryReqList[$sLabel]['URL']);
} else {
$this->error("request entry error: hostname is empty");
return false;
}
// POST向け処理
if ( isset($aryInfo['POST']) && is_array($aryInfo['POST']) ) {
foreach( $aryInfo['POST'] as $sKey => $sData ) {
$this->aryReqList[$sLabel]['POST'][$sKey] = $sData;
}
}
// キャッシュ
$this->aryReqList[$sLabel]['CACHE'] = $iCache;
// オプション
if ( isset($aryInfo['TIMEOUT']) && $aryInfo['TIMEOUT'] > 0 ) {
$this->aryReqList[$sLabel]['OPTIONS'][CURLOPT_CONNECTTIMEOUT] = $aryInfo['TIMEOUT'];
$this->aryReqList[$sLabel]['OPTIONS'][CURLOPT_TIMEOUT] = $aryInfo['TIMEOUT'];
} else {
$this->aryReqList[$sLabel]['OPTIONS'][CURLOPT_CONNECTTIMEOUT] = self::DEF_TIMEOUT;
$this->aryReqList[$sLabel]['OPTIONS'][CURLOPT_TIMEOUT] = self::DEF_TIMEOUT;
}
if ( isset($aryInfo['PORT']) ) {
$this->aryReqList[$sLabel]['OPTIONS'][CURLOPT_PORT] = $aryInfo['PORT'];
}
return true;
}
/**
* APIコール設定メソッド(QueryString)
*
* @access public
* @param string $sLabel API識別ラベル
* @param string $sUrl URL
* @param int $iCache キャッシュの有効時間
* @return boolean
*/
function addRequest($sLabel, $sUrl, $iCache=0) {
if ( empty($sLabel) || empty($sUrl) ) {
$this->error("request entry error: label or url is empty");
return false;
}
$aryUrl = preg_split("/\?/", $sUrl, 2);
if ( isset($aryUrl[1]) ) {
$aryParam = array();
$aryQuery = array();
parse_str($aryUrl[1], $aryParam);
foreach( $aryParam as $sKey => $sVal ) {
$aryQuery[] = $sKey."=".rawurlencode($sVal);
}
$sUrl = $aryUrl[0]."?".implode("&", $aryQuery);
}
$this->aryReqList[$sLabel] = array(
'URL' => $sUrl,
'URL_MD5' => $this->makeCacheKey($sUrl),
'CACHE' => $iCache,
'OPTIONS' => array(
CURLOPT_CONNECTTIMEOUT => self::DEF_TIMEOUT,
CURLOPT_TIMEOUT => self::DEF_TIMEOUT
)
);
return true;
}
/**
* オプション追加
*
* <pre>
* $aryArh = apache_request_headers();
* $objReq = new QueryRequest();
* $objReq->addRequest("MainApi", ...);
* $objReq->addOptions("MainApi", array(CURLOPT_COOKIE => $aryArh["Cookie"]));
* </pre>
*
* @access public
* @param string $sLabel API識別ラベル
* @param array $aryOpt オプション
* @return boolean
*/
function addOptions($sLabel, $aryOpt) {
if ( empty($sLabel) || !is_array($aryOpt) ) {
$this->error("request entry error: label or option is empty");
return false;
}
foreach( $aryOpt as $sKey => $sVal) {
$this->aryReqList[$sLabel]['OPTIONS'][$sKey] = $sVal;
}
return true;
}
/**
* タイプアウト設定
*
* @access public
* @param string $sLabel API識別ラベル
* @param integer $iTime タイムアウト値
* @return boolean
*/
function setTimeout($sLabel, $iTime) {
if ($iTime > 0 ) {
$this->aryReqList[$sLabel]['OPTIONS'][CURLOPT_CONNECTTIMEOUT] = $iTime;
$this->aryReqList[$sLabel]['OPTIONS'][CURLOPT_TIMEOUT] = $iTime;
}
}
/**
* POSTデータセット
*
* @access public
* @param string $sLabel API識別ラベル
* @param string $sKey POST送信したいデータ名
* @param string $sData POST送信したいデータ
* @return boolean
*/
function addPostData($sLabel, $sKey, $sData) {
// POST向け処理
if ( isset($this->aryReqList[$sLabel]) ) {
$this->aryReqList[$sLabel]['POST'][$sKey] = $sData;
}
}
/**
* APIコール実行
*
* @access public
* @return array
*/
function execute() {
$iApiCache = 0;
$iApiCall = 0;
$iApiTotal = count($this->aryReqList);
if ( $iApiTotal == 0 ) {
$this->error("execute error: no api call");
return null;
}
$objMulti = curl_multi_init();
$aryChannel = array();
foreach( $this->aryReqList as $sLabel => $aryParam ) {
// cacheが有効かつ cacheが存在する
if ( $aryParam["CACHE"] > 0 && $this->hasCache($aryParam["URL_MD5"]) ) {
// キャッシュデータの取得
$sBody = $this->getCache();
if ($sBody == "") {
$this->aryReqData[$sLabel] = null;
$this->aryReqList[$sLabel]["http_code"] = "404";
$this->removeCache($aryParam["URL_MD5"]);
} else {
$this->aryReqData[$sLabel] = $sBody;
$this->aryReqList[$sLabel]["http_code"] = "200";
$iApiCache++;
}
$this->info("DATA from cache - ".$aryParam["URL"]);
}
// cacheが無効または cacheが存在しない
else {
// 個々のリクエストの cURLチャンネルの登録
$objCurl = curl_init();
curl_setopt($objCurl, CURLOPT_URL, $aryParam['URL']);
curl_setopt($objCurl, CURLOPT_RETURNTRANSFER, 1);
if ( isset($aryParam["POST"]) ) {
curl_setopt($objCurl, CURLOPT_POST, TRUE);
curl_setopt($objCurl, CURLOPT_POSTFIELDS, $aryParam["POST"]);
$aryChannel[$sLabel]["CACHE"] = 0;
} else {
$aryChannel[$sLabel]["CACHE"] = $aryParam["CACHE"];
}
$aryChannel[$sLabel]["OBJECT"] = $objCurl;
$aryChannel[$sLabel]["URL"] = $aryParam["URL"];
$aryChannel[$sLabel]["URL_MD5"] = $aryParam["URL_MD5"];
if ( count($aryParam['OPTIONS']) != 0 ) {
curl_setopt_array($objCurl, $aryParam['OPTIONS']);
}
curl_multi_add_handle($objMulti, $objCurl);
$this->info("DATA from api - ".$aryParam["URL"]);
$iApiCall++;
}
}
$this->info("API Info - Total:".$iApiTotal
." Cache:".$iApiCache
." Call:".$iApiCall);
$sCacheStatus = "";
if ($this->bCache) {
$sCacheStatus .= "active";
} else {
$sCacheStatus .= "inactive";
}
if ($this->bGzCache) {
$sCacheStatus .= " compress";
}
$this->info("API cahce mode: ".$sCacheStatus);
if ($iApiCall == 0) {
return $this->aryResList;
}
// 並列にリクエストを処理(すべてのAPIの結果が返ってくるまで待ち、そしてパージ)
$active = null;
do {
$iRet = curl_multi_exec($objMulti, $active);
} while ($iRet == CURLM_CALL_MULTI_PERFORM);
while ($active && $iRet == CURLM_OK) {
if (curl_multi_select($objMulti) != -1) {
do {
$iRet = curl_multi_exec($objMulti, $active);
} while ($iRet == CURLM_CALL_MULTI_PERFORM);
}
}
// 受信結果を保存
foreach( $aryChannel as $sLabel => $aryCurl ) {
$sBody = curl_multi_getcontent($aryCurl["OBJECT"]);
$this->aryResList[$sLabel] = curl_getinfo($aryCurl["OBJECT"]);
$iHttpCode = $this->aryResList[$sLabel]['http_code'];
$this->aryReqData[$sLabel] = $sBody;
if ($iHttpCode == 200 && $sBody != "") {
// キャッシュへセット
if ( $this->bCache && $aryCurl["CACHE"] > 0 ) {
$this->setCache($aryCurl["URL_MD5"], $sBody, $aryCurl["CACHE"]);
}
}
curl_multi_remove_handle($objMulti, $aryCurl["OBJECT"]);
curl_close($aryCurl["OBJECT"]);
}
curl_multi_close($objMulti);
reset($this->aryReqList);
return($this->aryResList);
}
/**
* データの取得
*
* @access public
* @param string $sLabel API識別ラベル
* @return mixed
*/
function getData($sLabel='') {
if ( $sLabel == '' ) {
return $this->aryReqData;
} else if ( isset($this->aryReqData[$sLabel]) ) {
return $this->aryReqData[$sLabel];
} else {
return null;
}
}
/**
* レスポンス情報の取得
*
* @access public
* @param string $sLabel API識別ラベル
* @return array
*/
function getResponseInfo($sLabel='') {
if ( $sLabel != '' && isset($this->aryResList[$sLabel]) ) {
return $this->aryResList[$sLabel];
} else {
return $this->aryResList;
}
}
/**
* HTTPステータスの取得
*
* @access public
* @param string $sLabel API識別ラベル
* @return int
*/
function getHttpStatus($sLabel) {
if ( $sLabel != '' && isset($this->aryResList[$sLabel]) ) {
return $this->aryResList[$sLabel]['http_code'];
} else {
return -1;
}
}
/**
* リクエスト情報の取得
*
* @access public
* @param string $sLabel API識別ラベル
* @return array
*/
function getRequestInfo($sLabel='') {
if ( $sLabel != '' && isset($this->aryReqList[$sLabel]) ) {
return $this->aryReqList[$sLabel];
} else {
return $this->aryReqList;
}
}
/**
* キャッシュ格納のキーを作成
*
* @access public
* @param string $sUrl
* @return string
*/
function makeCacheKey($sUrl) {
$seed = get_class($this)."::";
return md5($seed.$sUrl);
}
/**
* キャッシュが存在するかチェック
*
* @access public
* @param string $key
* @return boolean
*/
function hasCache($key) {
$data = false;
if ($this->bCache) {
$data = apc_fetch($key);
}
if ($data !== false) {
$this->sCache = $data;
$this->info("has cache ".$key);
return true;
} else {
$this->sCache = null;
$this->info("no cache ".$key);
return false;
}
}
/**
* キャッシュの取得
*
* @access public
* @return string
*/
function getCache() {
if ($this->bGzCache) {
return gzuncompress($this->sCache);
} else {
return $this->sCache;
}
}
/**
* キャッシュへ保存
*
* @access public
* @param string $key
* @param string $content
* @param integer $ttl
* @return bool
*/
function setCache($key, $content, $ttl=0) {
// キャッシュの圧縮
if ($this->bGzCache) {
$content = gzcompress($content);
}
$bRet = false;
if ($this->bCache) {
$bRet = apc_store($key, $content, $ttl);
if ($bRet) {
$this->info("set cache as ".$key);
} else {
$this->error("set cache failed");
}
}
return $bRet;
}
/**
* キャッシュを削除
*
* @access public
* @param string $key
* @return bool
*/
function removeCache($key) {
$bRet = false;
if ($this->bCache) {
$bRet = apc_delete($key);
if ($bRet) {
$this->info("remove cache of ".$key);
} else {
$this->error("remove cache failed");
}
}
return $bRet;
}
/**
* キャッシュモード
*
* @access public
* @return void
*/
function enableCache() {
if ( function_exists("apc_store") ) {
$this->bCache = true;
}
}
/**
* キャッシュ圧縮モード
*
* @access public
* @return void
*/
function enableCompressCache() {
if ( function_exists("gzcompress") ) {
$this->bGzCache = true;
}
}
/**
* インフォメーションログ出力を有効にする
*
* @access public
* @return void
*/
public function showInfo() {
$this->bShowInfo = true;
}
/**
* エラーログ出力
*
* @access private
* @param string $sMsg
* @return void
*/
private function error($sMsg) {
error_log($sMsg, 0);
}
/**
* インフォメーションログ出力
*
* @access private
* @param string $sMsg
* @return void
*/
private function info($sMsg) {
if ($this->bShowInfo) {
error_log($sMsg, 0);
}
}
}
?>
このクラスを利用して、API を呼んでみます。以下のプログラムは、二種類の APIを呼んでいます。一つ目の APIの結果は5秒キャッシュされ、二つ目の API の結果は存在しないのでキャシュされることはありません。
<?php include("QueryRequest.php"); $objReq = new QueryRequest(); // キャッシュの設定 $objReq->enableCache(); $objReq->enableCompressCache(); // APIからのデータを圧縮してキャッシュ // URLによる APIサーバ指定 $sUrl = "http://headlines.yahoo.co.jp/rss/eiga_c_ent.xml"; $objReq->addRequest("MainApi", $sUrl, 5); // HASHによる APIサーバ指定 $aryInfo = array( "HOST" => "http://www.example.com/", // 必須 "URI" => "prog.cgi", // デフォルト "/" "TIMEOUT" => 8, // デフォルト 5秒 "PORT" => 80, // デフォルト 80 "PROTOCOL" => "http", // デフォルト "http" "QUERY" => array("key"=>"sub", "p"=>"快適") // デフォルト array() ); $objReq->addRequestHash("SubApi", $aryInfo, 5); // リクエスト発行 $objReq->execute(); // 取得データ if ($objReq->getHttpStatus("MainApi") == 200) { // 取得データ(XMLなど)を解析利用 $sMain = $objReq->getData("MainApi"); if ( preg_match("/<\?xml.*\?>/", $sMain, $aryMatch) ) { $xml = simplexml_load_string($sMain); foreach($xml->channel->item as $item) { echo (string)$item->title."\n"; } } } else { // メイン情報が取れないから リロードページ?404? error_log("404 ------",0); exit(0); } if ($objReq->getHttpStatus("SubApi") == 200) { // 処理 $sSub = $objReq->getData("SubApi"); } else { // 補足情報が取れないぐらいなら無視するなど・・・ } ?>
QueryRequestクラスで利用している parse_str() の挙動は、値を urldecode, htmlspecialchar同等の処理を通すのでいまいち使いにくいので似たような関数を実装した方が良いかもしれない。また、ログイン認証が必要な APIを呼び出す為に以下のようなメソッドを用意する必要があるかもしれない。。。POST時の扱いももう少し検討が必要かなあ〜
function setUser($sLabel, $sUser, $sPasswd) { if ( empty($sLabel) || empty($sUser) || empty($sPasswd) ) { $this->error("request entry error: label or user or password is empty"); return false; } $this->aryReqList[$sLabel]['OPTIONS'][CURLOPT_USERPWD] = $sUser.":".$sPasswd; return true; }