PHPでAmazon PAAPI v5を使って商品情報を取得したり検索する

過去にもAmazon PAAPI v5のサンプルコードは掲載しているのですが、scratchpad※1の紹介ついでだったり、商品検索ウィジェットの代替品※2のついでだったりして読みづらいのであらためて書き直しました。

実際、数週間も経つと書いた本人でも忘れるので自分のブログを読み返してコードをコピペしているのですが、過去記事のサンプルだと長過ぎて抜き取る部分を間違えてしまい、なぜ動作しないのか小一時間悩んじゃったんですよね…。

共通クラス

Amazon公式のスクラッチパッドで出力されるクラスなので、商品情報の取得(GetItems)でも検索(SearchItems)でも使います。

class AwsV4 {
    private $accessKey = null;
    private $secretKey = null;
    private $path = null;
    private $regionName = null;
    private $serviceName = null;
    private $httpMethodName = null;
    private $queryParametes = array ();
    private $awsHeaders = array ();
    private $payload = "";
    private $HMACAlgorithm = "AWS4-HMAC-SHA256";
    private $aws4Request = "aws4_request";
    private $strSignedHeader = null;
    private $xAmzDate = null;
    private $currentDate = null;

    public function __construct($accessKey, $secretKey) {
        $this->accessKey = $accessKey;
        $this->secretKey = $secretKey;
        $this->xAmzDate = $this->getTimeStamp ();
        $this->currentDate = $this->getDate ();
    }
    function setPath($path) {
        $this->path = $path;
    }
    function setServiceName($serviceName) {
        $this->serviceName = $serviceName;
    }
    function setRegionName($regionName) {
        $this->regionName = $regionName;
    }
    function setPayload($payload) {
        $this->payload = $payload;
    }
    function setRequestMethod($method) {
        $this->httpMethodName = $method;
    }
    function addHeader($headerName, $headerValue) {
        $this->awsHeaders [$headerName] = $headerValue;
    }
    private function prepareCanonicalRequest() {
        $canonicalURL = "";
        $canonicalURL .= $this->httpMethodName . "\n";
        $canonicalURL .= $this->path . "\n" . "\n";
        $signedHeaders = '';
        foreach ( $this->awsHeaders as $key => $value ) {
            $signedHeaders .= $key . ";";
            $canonicalURL .= $key . ":" . $value . "\n";
        }
        $canonicalURL .= "\n";
        $this->strSignedHeader = substr ( $signedHeaders, 0, - 1 );
        $canonicalURL .= $this->strSignedHeader . "\n";
        $canonicalURL .= $this->generateHex ( $this->payload );
        return $canonicalURL;
    }
    private function prepareStringToSign($canonicalURL) {
        $stringToSign = '';
        $stringToSign .= $this->HMACAlgorithm . "\n";
        $stringToSign .= $this->xAmzDate . "\n";
        $stringToSign .= $this->currentDate . "/" . $this->regionName . "/" . $this->serviceName . "/" . $this->aws4Request . "\n";
        $stringToSign .= $this->generateHex ( $canonicalURL );
        return $stringToSign;
    }
    private function calculateSignature($stringToSign) {
        $signatureKey = $this->getSignatureKey ( $this->secretKey, $this->currentDate, $this->regionName, $this->serviceName );
        $signature = hash_hmac ( "sha256", $stringToSign, $signatureKey, true );
        $strHexSignature = strtolower ( bin2hex ( $signature ) );
        return $strHexSignature;
    }
    public function getHeaders() {
        $this->awsHeaders ['x-amz-date'] = $this->xAmzDate;
        ksort ( $this->awsHeaders );
        // Step 1: CREATE A CANONICAL REQUEST
        $canonicalURL = $this->prepareCanonicalRequest ();
        // Step 2: CREATE THE STRING TO SIGN
        $stringToSign = $this->prepareStringToSign ( $canonicalURL );
        // Step 3: CALCULATE THE SIGNATURE
        $signature = $this->calculateSignature ( $stringToSign );
        // Step 4: CALCULATE AUTHORIZATION HEADER
        if ($signature) {
            $this->awsHeaders ['Authorization'] = $this->buildAuthorizationString ( $signature );
            return $this->awsHeaders;
        }
    }
    private function buildAuthorizationString($strSignature) {
        return $this->HMACAlgorithm . " " . "Credential=" . $this->accessKey . "/" . $this->getDate () . "/" . $this->regionName . "/" . $this->serviceName . "/" . $this->aws4Request . "," . "SignedHeaders=" . $this->strSignedHeader . "," . "Signature=" . $strSignature;
    }
    private function generateHex($data) {
        return strtolower ( bin2hex ( hash ( "sha256", $data, true ) ) );
    }
    private function getSignatureKey($key, $date, $regionName, $serviceName) {
        $kSecret = "AWS4" . $key;
        $kDate = hash_hmac ( "sha256", $date, $kSecret, true );
        $kRegion = hash_hmac ( "sha256", $regionName, $kDate, true );
        $kService = hash_hmac ( "sha256", $serviceName, $kRegion, true );
        $kSigning = hash_hmac ( "sha256", $this->aws4Request, $kService, true );
        return $kSigning;
    }
    private function getTimeStamp() {
        return gmdate ( "Ymd\THis\Z" );
    }
    private function getDate() {
        return gmdate ( "Ymd" );
    }
}

ASINから商品情報を取得

PHPコード

//Amazon PAAPIの設定
$cfg['AWSAccessKeyId'] = "[アクセスキー]";
$cfg['AWSSecretAccessKeyId'] = "[シークレットアクセスキー]";
$cfg['AssociateTag'] = "[アソシエイトタグ]";

function getAmazonItemLookUp($item_id) {
	global $cfg;
	$serviceName="ProductAdvertisingAPI";
	$region="us-west-2";
	$accessKey=$cfg['AWSAccessKeyId'];
	$secretKey=$cfg['AWSSecretAccessKeyId'];
	$payload="{"
			." \"ItemIds\": ["
			."  ".$item_id.""
			." ],"
			." \"Resources\": ["
			."  \"Images.Primary.Small\","
			."  \"Images.Primary.Medium\","
			."  \"Images.Primary.Large\","
			."  \"Offers.Summaries.LowestPrice\","
			."  \"BrowseNodeInfo.BrowseNodes\","
			."  \"BrowseNodeInfo.BrowseNodes.Ancestor\","
			."  \"ItemInfo.Title\","
			."  \"ItemInfo.ByLineInfo\","
			."  \"ItemInfo.ManufactureInfo\","
			."  \"ItemInfo.Features\","
			."  \"ItemInfo.ProductInfo\""
			." ],"
			." \"PartnerTag\": \"".$cfg['AssociateTag']."\","
			." \"PartnerType\": \"Associates\","
			." \"Marketplace\": \"www.amazon.co.jp\""
			."}";
	$host="webservices.amazon.co.jp";
	$uriPath="/paapi5/getitems";
	$awsv4 = new AwsV4 ($accessKey, $secretKey);
	$awsv4->setRegionName($region);
	$awsv4->setServiceName($serviceName);
	$awsv4->setPath ($uriPath);
	$awsv4->setPayload ($payload);
	$awsv4->setRequestMethod ("POST");
	$awsv4->addHeader ('content-encoding', 'amz-1.0');
	$awsv4->addHeader ('content-type', 'application/json; charset=utf-8');
	$awsv4->addHeader ('host', $host);
	$awsv4->addHeader ('x-amz-target', 'com.amazon.paapi5.v1.ProductAdvertisingAPIv1.GetItems');
	$headers = $awsv4->getHeaders ();
	$headerString = "";
	foreach ( $headers as $key => $value ) {
		$headerString .= $key . ': ' . $value . "\r\n";
	}
	$params = array (
			'http' => array (
				'header' => $headerString,
				'method' => 'POST',
				'content' => $payload
			)
		);
	$stream = stream_context_create ( $params );
	$fp = @fopen ( 'https://'.$host.$uriPath, 'rb', false, $stream );
	if (! $fp) {
		throw new Exception ( "Exception Occured" );
	}
	$response = @stream_get_contents ( $fp );
	if ($response === false) {
		throw new Exception ( "Exception Occured" );
	}
	return $response;
}

解説

これもほとんどスクラッチパッドで出力されたコードなので細かい解説はしませんが、getAmazonItemLookUpという関数を作って、引数に複数のASINを渡せるようにしています。

使用例

getAmazonItemLookUp('"4865940650","B08X9XY72Z","479813547X"');

最大で10個までASINを指定できます。そのままPAAPIに渡しているだけなので、個々のASINはダブルクォーテーションで囲む必要がある点に注意。

尚、11個以上指定するとエラーになってしまうため、実運用において大量の商品情報を更新したい場合は1時間に1回、10個分抽出してAPIを叩く、みたいなことをしています。

特定カテゴリの商品一覧を取得する

ここでいうカテゴリとはAmazonのブラウズノードのことです。

PHPコード

//Amazon PAAPIの設定
$cfg['AWSAccessKeyId'] = "[アクセスキー]";
$cfg['AWSSecretAccessKeyId'] = "[シークレットアクセスキー]";
$cfg['AssociateTag'] = "[アソシエイトタグ]";

function getAmazonSearchItems($keywords = null,$browsenode = null, $page = 1)
{
	global $cfg;
	$serviceName="ProductAdvertisingAPI";
	$region="us-west-2";
	$accessKey=$cfg['AWSAccessKeyId'];
	$secretKey=$cfg['AWSSecretAccessKeyId'];

	$payload = '{'.
				' "PartnerTag": "'.$cfg['AssociateTag'].'",'.
				' "PartnerType": "Associates",'.
				' "Marketplace": "www.amazon.co.jp",'.
				' "SortBy":"Relevance",'. //NewestArrivals
				' "ItemPage":'.$page.','.
				' "Resources": ['.
				' "Images.Primary.Small",'.
				' "Images.Primary.Medium",'.
				' "Images.Primary.Large",'.
				' "Offers.Summaries.LowestPrice",'.
				' "ItemInfo.Title",'.
				' "ItemInfo.ByLineInfo",'.
				' "ItemInfo.ManufactureInfo",'.
				' "ItemInfo.ProductInfo",'.
				' "BrowseNodeInfo.BrowseNodes",'.
				' "BrowseNodeInfo.BrowseNodes.Ancestor"'.
				' ]';
	if (!is_null($browsenode)) {
		$payload .= ',"BrowseNodeId":"'.$browsenode.'"';
	}
	if (!is_null($keywords)) {
		$payload .= ',"Keywords":"'.$keywords.'"';
	}
	$payload .= '}';

	$host="webservices.amazon.co.jp";
	$uriPath="/paapi5/getitems";
	$awsv4 = new AwsV4 ($accessKey, $secretKey);
	$awsv4->setRegionName($region);
	$awsv4->setServiceName($serviceName);
	$awsv4->setPath ($uriPath);
	$awsv4->setPayload ($payload);
	$awsv4->setRequestMethod ("POST");
	$awsv4->addHeader ('content-encoding', 'amz-1.0');
	$awsv4->addHeader ('content-type', 'application/json; charset=utf-8');
	$awsv4->addHeader ('host', $host);
	$awsv4->addHeader ('x-amz-target', 'com.amazon.paapi5.v1.ProductAdvertisingAPIv1.SearchItems');
	$headers = $awsv4->getHeaders ();
	$headerString = "";
	foreach ( $headers as $key => $value ) {
		$headerString .= $key . ': ' . $value . "\r\n";
	}
	$params = array (
			'http' => array (
				'header' => $headerString,
				'method' => 'POST',
				'content' => $payload
			)
		);
	$stream = stream_context_create ( $params );
	$fp = @fopen ( 'https://'.$host.$uriPath, 'rb', false, $stream );
	if (! $fp) {
		throw new Exception ( "Exception Occured" );
	}
	$response = @stream_get_contents ( $fp );
	if ($response === false) {
		throw new Exception ( "Exception Occured" );
	}
	return $response;
}

解説

こちらもスクラッチパッドのコードから(略

ただ、ASINから取得する場合とキーワード(もしくはブラウズノード)を指定する場合とでpayloadの部分だけを書き換えるだけやん、とか思ってしまうとハマります。ぼくはハマりました。

その下のaddHeaderの部分がちょっと違うんですよね。

■GetItemsの場合

$awsv4->addHeader ('x-amz-target', 'com.amazon.paapi5.v1.ProductAdvertisingAPIv1.GetItems');

■SearchItemsの場合

$awsv4->addHeader ('x-amz-target', 'com.amazon.paapi5.v1.ProductAdvertisingAPIv1.SearchItems');

昔のAmazon APIはXMLで返ってきて、その中にエラーメッセージもあったため、「あ、渡すパラメータ間違えてた(*ノω・*)テヘ」とすぐに気がついたのですが、PAAPIv5になってからエラーのときは何も返さないのでこんな単純な間違いでもハマるときはハマるんですよねぇ。

…1時間近く悩みました。

使用例

getAmazonSearchItems('PHPプログラミング');

キーワードだけを指定すると、全カテゴリを対象に検索をします。

getAmazonSearchItems(null, '3232342051');

2番目の引数(ブラウズノード)だけを指定すると特定のカテゴリ、上記の例では3232342051=書籍Webプログラミングにある商品一覧を取得できます。

ブラウズノード番号はAPIで取得する方法もありますが、面倒なのでぼくはAmazonのページを開いてURLを見てブラウズノードを確認しています。それっぽい10桁の数字なのですぐわかるハズ。

補足説明

条件を指定した検索なので、ソート順の指定も可能です。

ソース中にも書いてありますが、SortByの部分をRelevanceにすれば関連度の高い順、NewestArrivalsにすれば新着順にできます。

どんなソート順が選べるのかは下記のマニュアルを参照すると良いでしょう。

https://webservices.amazon.com/paapi5/documentation/search-items.html#sortby-parameter

また、Amazon PAAPI v5が返してくれる商品情報の最大値は10個と決まっていますが、ItemPageパラメーターでページ番号を指定できるため、1~10ページまで繰り返しリクエストすることで100件取得したりすることも可能です。

ぼくはわりとこの機能を使うので、getAmazonSearchItems関数の3番目の引数にページ番号を持たせています。

連続リクエスト回数には注意が必要ですけどね。

Amazonから渡されたJSONをデコードする

上述したgetAmazonItemLookUpもgetAmazonSearchItemsも返り値はAmazonから戻されるJSONです。

そのため、json_decodeでデコードしてクラスに戻してから使いましょう。

PHPコード例

$json = getAmazonSearchItems('PHPプログラミング');
$aws = json_decode($json);
foreach ($aws->SearchResult->Items as $item) {
	echo $item->ItemInfo->Title->DisplayValue;
	echo $item->ItemInfo->ByLineInfo->Brand->DisplayValue;
	echo $item->ItemInfo->ManufactureInfo->Model->Label;
}

まとめ

Amazon PAAPI v5の中で、恐らく最も使うであろうGetItemsとSearchItemsについて、あらためてサンプルコードにしてみました。

これで記憶喪失になっても安心!

adsbygoogle

フォロー