目次
はじめに
つい最近になってAmazon Product Advertising API(PA-API)がバージョン4から5にアップデートしていて、バージョン4はもう使えなくなるという事実を知りました。今までメールが広告扱いになっていて見逃していたため慌てて修正する羽目になってしまいました。
ヘルプ > Product Advertising API について
これを機に多少V5を触ったのでPHPではありますが、その使い方の備忘録を兼ねてやり方を残しておこうと思います。そして、今回はタイトルのように「Kindleマンガの売れ筋ランキング」を取得してきてその一覧(10個)表示しようと思います。
SDKは諦めました
いきなり否定的で不吉な見出しですが、Amazonで提供しているPHPのSDKで頑張るのは諦めました。SDKは以下のリンクから入手することができます。
Using SDK - Product Advertising API 5.0
ダウンロードしてサーバーに配置してちょっといじりましたが一番初めの方のサンプルが動かず、やっと原因が分かったと思い解消しても次のエラーが出てきたため環境構築の段階ででめんどくさくなって止めました。
エラー1
冒頭の名前空間の大文字小文字ミス?(GetBrowseNodes.php)
↓
(正)use Amazon\ProductAdvertisingAPI\v1\com\amazon\paapi5\v1\api\DefaultApi;
エラー2
「Call to undefined function GuzzleHttp\_idn_uri_convert()」のエラー。
付属のcomposer.jsonに従ってライブラリをインストールしていかなければならないようですが、レンタルサーバー使っているのでできるか迷ってやめました。(調べてみるとできそうなのでこれを見てもあきらめないでください)
HTTPS POSTで取得する
雲行きが怪しくなってきましたが、公式でScratchpadというPA-APIを動かせるサンプルページを見つけました。色々検索条件を変更しながら実際に動かすことができて、出力されるJSONやソースコードを確認することができるという素晴らしいページです。ソースコード自体はAmazonのサーバーにPOSTするだけで何かに依存しているわけではないため、出てくるコードをコピペしてPHP動かせばあっという間に完成します。これを使って「Kindleマンガの売れ筋ランキング」一覧を表示してみます。
使い方はまずページの初めにタグやアクセスキー、シークレットキーを入力します。(※アクセスキー、シークレットキーの取得方法は省略します)
今回は売れ筋マンガのBrowseNodeIdを使ってその一覧を取得したいためまずは左列の「SearchItems」を選んだ後に、下記の売れ筋ランキングを開いてそのURLに出ているID「2293143051」をコピーすることでBrowseNodeIdをゲットします。
Keywordに「*」(アスタリスク)を記入して、「Resources」はSelect Allにチェックを入れます。下のほうにある「Add a new parameter」でBrowseNodeIdを選択して「+」ボタンを押して追加したら「2293143051」を記入します。(ちなみにこれに気づくのに小一時間かかりました…)。そして「Run request」を押します。
出てきたのがこちらになります。Response typesの「HTML response」タブからコピペしてきたものです(iframeで表示しています)。
そして、JSON responseタブにはもちろんJSONのレスポンスが入っているわけですが、最終的にはこの内容を参考にしながらPHPで解釈する文法を書いていくという形になります。つまりあとはjson_decodeするだけ。
さらにこの下の「Code snippets」にはこのJSONを出力を出した時のPHPソースコードもしくはJAVAソースコードが掲載されていて、例えば私のケースですとPHPソースコードをそのままコピペしてサーバーにアップすれば同じJSONレスポンスを得ることができるわけです。ただ、あくまでこの結果はJSONなので一旦配列に直してページに出力する手間がかかります。以下に画像とタイトルリンクを表示するサンプルを載せます。
サンプルコード
<?php /* Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ /* Licensed under the Apache License, Version 2.0. */ // Put your Secret Key in place of ********** $serviceName="ProductAdvertisingAPI"; $region="us-west-2"; $accessKey="{{ 自分のアクセスキー }}"; $secretKey="**********"; $payload="{" ." \"Keywords\": \"*\"," ." \"Resources\": [" ." \"BrowseNodeInfo.BrowseNodes\"," ." \"BrowseNodeInfo.BrowseNodes.Ancestor\"," ." \"BrowseNodeInfo.BrowseNodes.SalesRank\"," ." \"BrowseNodeInfo.WebsiteSalesRank\"," ." \"CustomerReviews.Count\"," ." \"CustomerReviews.StarRating\"," ." \"Images.Primary.Small\"," ." \"Images.Primary.Medium\"," ." \"Images.Primary.Large\"," ." \"Images.Variants.Small\"," ." \"Images.Variants.Medium\"," ." \"Images.Variants.Large\"," ." \"ItemInfo.ByLineInfo\"," ." \"ItemInfo.ContentInfo\"," ." \"ItemInfo.ContentRating\"," ." \"ItemInfo.Classifications\"," ." \"ItemInfo.ExternalIds\"," ." \"ItemInfo.Features\"," ." \"ItemInfo.ManufactureInfo\"," ." \"ItemInfo.ProductInfo\"," ." \"ItemInfo.TechnicalInfo\"," ." \"ItemInfo.Title\"," ." \"ItemInfo.TradeInInfo\"," ." \"Offers.Listings.Availability.MaxOrderQuantity\"," ." \"Offers.Listings.Availability.Message\"," ." \"Offers.Listings.Availability.MinOrderQuantity\"," ." \"Offers.Listings.Availability.Type\"," ." \"Offers.Listings.Condition\"," ." \"Offers.Listings.Condition.SubCondition\"," ." \"Offers.Listings.DeliveryInfo.IsAmazonFulfilled\"," ." \"Offers.Listings.DeliveryInfo.IsFreeShippingEligible\"," ." \"Offers.Listings.DeliveryInfo.IsPrimeEligible\"," ." \"Offers.Listings.DeliveryInfo.ShippingCharges\"," ." \"Offers.Listings.IsBuyBoxWinner\"," ." \"Offers.Listings.LoyaltyPoints.Points\"," ." \"Offers.Listings.MerchantInfo\"," ." \"Offers.Listings.Price\"," ." \"Offers.Listings.ProgramEligibility.IsPrimeExclusive\"," ." \"Offers.Listings.ProgramEligibility.IsPrimePantry\"," ." \"Offers.Listings.Promotions\"," ." \"Offers.Listings.SavingBasis\"," ." \"Offers.Summaries.HighestPrice\"," ." \"Offers.Summaries.LowestPrice\"," ." \"Offers.Summaries.OfferCount\"," ." \"ParentASIN\"," ." \"RentalOffers.Listings.Availability.MaxOrderQuantity\"," ." \"RentalOffers.Listings.Availability.Message\"," ." \"RentalOffers.Listings.Availability.MinOrderQuantity\"," ." \"RentalOffers.Listings.Availability.Type\"," ." \"RentalOffers.Listings.BasePrice\"," ." \"RentalOffers.Listings.Condition\"," ." \"RentalOffers.Listings.Condition.SubCondition\"," ." \"RentalOffers.Listings.DeliveryInfo.IsAmazonFulfilled\"," ." \"RentalOffers.Listings.DeliveryInfo.IsFreeShippingEligible\"," ." \"RentalOffers.Listings.DeliveryInfo.IsPrimeEligible\"," ." \"RentalOffers.Listings.DeliveryInfo.ShippingCharges\"," ." \"RentalOffers.Listings.MerchantInfo\"," ." \"SearchRefinements\"" ." ]," ." \"BrowseNodeId\": \"2293143051\"," ." \"PartnerTag\": \"{{ 自分のタグ }}\"," ." \"PartnerType\": \"Associates\"," ." \"Marketplace\": \"www.amazon.co.jp\"" ."}"; $host="webservices.amazon.co.jp"; $uriPath="/paapi5/searchitems"; $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" ); } // echo $response; $results = json_decode($response,true); $items = $results["SearchResult"]["Items"]; foreach($items as $item) { $tmp = array(); $tmp["title"] = $item["ItemInfo"]["Title"]["DisplayValue"]; $tmp["url"] = $item["DetailPageURL"]; $tmp["imgurl"] = $item["Images"]["Primary"]["Large"]["URL"]; echo "<img src='".$tmp["imgurl"]."' /><br>"; echo "<a href='".$tmp["url"]."'>".$tmp["title"]."</a><br>"; } 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" ); } } ?>
AwsV4はSearchItemsだけでなくGetItemsやGetBrowseNodesでも登場する共通クラスのようです。適当に分離してincludeするようにしたほうが良いように思います。
「echo $response;」のコメントアウト箇所からAwsV4までの定義が適当に書いたソースの個所です。念のため抜き出しておきます。(適当なので参考程度にしてください)
// echo $response; $results = json_decode($response,true); $items = $results["SearchResult"]["Items"]; foreach($items as $item) { $tmp = array(); $tmp["title"] = $item["ItemInfo"]["Title"]["DisplayValue"]; $tmp["url"] = $item["DetailPageURL"]; $tmp["imgurl"] = $item["Images"]["Primary"]["Large"]["URL"]; echo "<img src='".$tmp["imgurl"]."' /><br>"; echo "<a href='".$tmp["url"]."'>".$tmp["title"]."</a><br>"; }
これはSearchItemsの結果なので初めの配列キーにそのように指定していますが、GetItemsではまた異なるキー("ItemsResult")なので注意してください。
画像を自サーバに保存するのは規約上NGなのでそのまま出力するようにしています。
サンプル実行結果
いい感じに出てきてくれました。ありがたいです。(これはPHPで出力した結果をコピペしたものです)
鬼滅の刃 19 (ジャンプコミックスDIGITAL)
鬼滅の刃 17 (ジャンプコミックスDIGITAL)
銀の匙 Silver Spoon(15) (少年サンデーコミックス)
ゴブリンスレイヤー 9巻 (デジタル版ビッグガンガンコミックス)
亜人ちゃんは語りたい(8) (ヤングマガジンコミックス)
鬼滅の刃 12 (ジャンプコミックスDIGITAL)
鬼滅の刃 15 (ジャンプコミックスDIGITAL)
鬼滅の刃 10 (ジャンプコミックスDIGITAL)
SPY×FAMILY 2 (ジャンプコミックスDIGITAL)
鬼滅の刃 1 (ジャンプコミックスDIGITAL)
(補足)ソートの変更やページ番号はどうするの?
すでにお気づきかもしれませんが、途中の過程でBroseNodeIdを追加しましたが、その他にもソートやページ番号などの条件を記載することができます。
そしてこの変更は「$payload」の配列の変数に反映されます。つまり、PHPで使う場合はこの$payload変数を動的に変えるような作りにすると使いやすくなるように思います(関数にするとか)。BrowseNodeIdも加えて多少並びを変えて抜粋してくると、こんな感じになっています。BrowseNodeIdとSortBy、ItemPageの部分です。
(省略) $payload="{" ." \"Keywords\": \"*\"," ." \"BrowseNodeId\": \"2293143051\"," ." \"SortBy\": \"AvgCustomerReviews\"," ." \"ItemPage\": 1," ." \"Resources\": [" (省略)
また、ソートの種類については以下を参考にしてください。V4とは多少変わっています。この「AvgCustomerReviews」はレビュー平均値が高い順のようです。
一応このときの結果を載せておきます。
さいごに
PHPを使ってPA-API V5を頑張りました。SDKはちょっとわかりづらいので諦めましたが、スクラッチパッドで割と簡単にできたので諦めなくてよかったです。
あと、途中には書きませんでしたが、実はV5はV4と完全互換ではなく取れなくなったデータがあります(逆もありますが)。以下のリンク参考にしてください。