【コピペで使える】Amazon API 5.0 (PA-API 5.0)を使って商品キーワード検索を行うPHPクラスサンプルコード

プログラミング全般

JANコード検索サイトにも使っているAmazon API(商品情報API)の検索クラスのソースコードです。

PHP環境やWordPress環境などでそのまま動かせるので参考までにご紹介します。

PAAPI商品検索クラス サンプルコード(PHP)

ファイル構成

以下の4ファイル構成となっています。

class.PAAPISearch.php ・・・Amazonから検索データを取得するコントローラー
class.AwsV4.php ・・・PAAPI用の汎用クラス(Amazon公式から抜粋)
paapi-payload-searchitems.php ・・・PAAPIに渡すペイロードを返す関数(Amazon公式から抜粋)
main.php ・・・使い方のサンプル

「class.PAAPISearch.php」ファイルが今回新規作成したコードで、Amazonからのデータ取得やチェックを行うクラスとなっています。

また、もうひとつ「class.AwsV4.php」にクラスが定義されていますがこちらはAmazon Product Advertising API 5.0(商品情報API)のAPIリファレンスページからアクセスできるScratchpadでAmazonが提供しているサンプルコードをほぼそのまま使用しています。

同じく「paapi-payload-searchitems.php」もScratchpadから取得したデータが中心になっており、検索用の引数を渡すための関数としています。これらの引数はペイロードと呼ばれており、ペイロードの内容を変えることでキーワード検索と特定商品の詳細情報の取得の切り替えや、検索時の取得件数やページ番号の指定などを行うことができます。

今回はペイロードの内容を固定して内容を取得するサンプルとなっていますが、このペイロードを生成する関数を詳細化することでページ送りやカテゴリの絞り込み検索などの機能追加が可能です。

最後に、main.phpファイルは実環境で表示を行うビューをイメージしたサンプルファイルです。上記3つのファイルをインクルードし、インスタンス化したクラスから検索結果を取得して、その内容をHTMLにパースし出力します。出力するHTMLはサンプルとして最低限の記述しかないため、各環境でアレンジしてみてください。

実行手順

4つのファイルは同じディレクトリに配置された状態で動くようになっています。require_onceの指定パスなどを編集すればファイルの配置を変えることも可能です。

// main.php
require_once 'class.PAAPISearch
.php'; //PAAPI検索クラス
require_once 'class.AwsV4.php'; //AwsV4クラス
require_once 'paapi-payload-searchitems.php'; //PAAPI検索ペイロード

また、PA-APIを利用するには、Amazonアソシエイトのアカウント登録が必要なほか、認証キーなどのユーザー固有(アプリ固有)の情報が必要となります。「class.PAAPISearch.php」にこれらの内容を記述する行がありますので、この内容をご自身の情報に書き換えて使用してください。

    protected $access_key_id         = 'XXXXXXXXXXXXXXXXXXXX'; //Amazonアクセスキー
    protected $secret_access_key     = 'xxXxxxXxxxxxXxxXXXxxXXXxxXXxxXxxxXXXxxXX'; //Amazonシークレットキー
    protected $associate_tracking_id = 'xxxxx-22'; //Amazonアソシエイトタグ

書き換えが必要なのは「Amazonアクセスキー(20文字程度の英数字)」「Amazonシークレットキー(40文字程度の英数字)」「Amazonアソシエイトタグ(ご自身で設定したタグ)」の3つです。

実行イメージ

dl、dt、ddタグのリストがそのまま出力されるだけなのでスタイルはいろいろアレンジしてみてください。画像のURLを取り出せば商品画像を表示することも可能です。

<html>
<body>
<dl>
<dt>商品名</dt><dd><a href="https://www.amazon.co.jp/dp/B0711PVX6Z?tag=cravel-22&amp;linkCode=osi&amp;th=1&amp;psc=1">Amazonベーシック USBケーブル 0.9m (2.0タイプAオス - マイクロBケーブル) ブラック</a>
<dt>ASIN</dt><dd>B0711PVX6Z</dd>
<dt>JANコード</dt><dd>0841710182286</dd>
<dt>価格</dt><dd>¥604</dt>
</dl>

<dl>
<dt>商品名</dt><dd><a href="https://www.amazon.co.jp/dp/B071D8THD2?tag=cravel-22&amp;linkCode=osi&amp;th=1&amp;psc=1">Anker USB Type C ケーブル PowerLine USB-C &amp; USB-A 3.0 ケーブル Xperia / Galaxy / LG / iPad Pro MacBook その他 Android 等 USB-C機器対応 テレワーク リモート 在宅勤務 0.9m ホワイト</a>
<dt>ASIN</dt><dd>B071D8THD2</dd>
<dt>JANコード</dt><dd>0848061034721</dd>
<dt>価格</dt><dd>¥699</dt>
</dl>

<dl>
<dt>商品名</dt><dd><a href="https://www.amazon.co.jp/dp/B0871ZKR8C?tag=cravel-22&amp;linkCode=osi&amp;th=1&amp;psc=1">Rampow Micro USB ケーブル【2M/保証付き/黒】 2.4A急速充電ケーブル 高速データ転送 コントローラー対応 Android多機種スマホ対応 スマホ 充電ケーブル マイクロusbケーブル</a>
<dt>ASIN</dt><dd>B0871ZKR8C</dd>
<dt>JANコード</dt><dd></dd>
<dt>価格</dt><dd>¥699</dt>
</dl>

...

</body>
</html>

サンプルではAPIから取得したレスポンスから商品名、商品URL、ASI、JANコード、価格を取得して表示しています。

APIが取得したその他のデータを出力したい場合は$itemに入っている各値を取り出してあげればOKです。

$item->Images->Primary->Large->URL //商品画像のURL
$item->ItemInfo->ByLineInfo->Brand->DisplayValue //ブランド
$item->ItemInfo->ProductInfo->ReleaseDate->DisplayValue //発売日
$item->Offers->Listings[0]->Availability->Message //在庫

また、今回の例ではPA-APIで取得可能なリソースをすべて取得するような設定としています。不要なデータはトラフィックを増加させてしまうため、実際に利用する際はペイロードを変更して必要最低限のリソースを取得するとよいと思います。

リソースの詳細はリファレンスが公開されています。(英語のみ?)

ソースコード

以下より、ファイルごとのPHPソースコードです。

class.PAAPISearch.php

<?php
if ( !class_exists( 'PAAPISearch
' ) ):
class PAAPISearch
{
    public $kw = null;
 //検索キーワード
    public $response = null;
 //APIから取得したJSONをここに格納
    public $message = null;
 //API取得時のエラーメッセージなどを格納

    protected $access_key_id         = (Amazonアクセスキー
); //Amazonアクセスキー
    protected $secret_access_key     = (Amazonシークレットキー
); //Amazonシークレットキー
    protected $associate_tracking_id = (Amazonアソシエイトタグ
); //Amazonアソシエイトタグ
    protected $err_msg = array(
 //表示用のメッセージを格納
        'notfound' => '見つかりませんでした。',
    );

    // API取得メソッド
    public function get_response() {
        $this->get_new_response_from_amazon();
        return $this->response;
    }


    // Amazon API(PAAPI5) 新規レスポンス取得
    private function get_new_response_from_amazon() {
        $serviceName = "ProductAdvertisingAPI";
        $region = "us-west-2";
        $accessKey = $this->access_key_id;
        $secretKey = $this->secret_access_key;
        $associate_tracking_id = $this->associate_tracking_id;

        $this->message = null; //メッセージを空に

        // キーワード検索用Payload取得
        $payload = get_paapi_payload_searchitems($associate_tracking_id, $this->kw);
        $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) { //ERROR:fopen
            $this->error('fopenに失敗しました');
            return false;
        }

        // レスポンス取得
        $response = @stream_get_contents($fp);

        if($this->check_response_error($response)) { //エラーチェック
            // レスポンスをプロパティに格納して終了
            $this->response = $response;
            return true;
        }

        // 以下はエラー時のフロー。とりあえず表向きには見つかりませんでしたのエラーを返す。
        $this->response = null;
        $this->message = $this->err_msg['notfound'];
        return false;
    }


    // レスポンスのエラーチェック
    private function check_response_error($response = null) {
        switch ($this->service) {
            case 'amazon':
                return $this->check_response_error_for_amazon($response);
                break;
            default:
                return $this->check_response_error_for_amazon($response);
                break;
        }
    }


    // レスポンスのエラーチェック。エラーがあればerrorプロパティに格納
    private function check_response_error_for_amazon($response = null) {
        //ERROR:stream_get_contents()
        if ($response === false) {
            $this->error('stream_get_contents()に失敗');
            return false;
        }
        //ERROR:503
        if (includes_string($response, 'Website Temporarily Unavailable')) {
            $this->error('503エラー');
            return false;
        }
        //jsonにエラーがあるかを確認する
        $json = json_decode($response);
        if (property_exists($json, 'Errors')) { //json内にerrorが含まれる場合
            $json_error_code    = $json->{'Errors'}[0]->{'Code'};
            $json_error_message = $json->{'Errors'}[0]->{'Message'};
            $this->error('APIエラー:'.$json_error_code.' / '.$json_error_message);
            return false;
        }
        //ERROR:No SeaechResult
        if (!property_exists($json, 'SearchResult')) {
            $this->error('SearchResultが存在しません');
            return false;
        }
        return true;
    }

    private function error($msg = '') {
        // APIエラー発生時のメッセージが$msgで送られてくるので必要に応じてログ記録などの処理を行ってください。
        echo $msg;
    }
}
endif;
?>

class.AwsV4.php

<?php
// Amazon API用クラス
if ( !class_exists( 'AwsV4' ) ):
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" );
}
}
endif;
?>

paapi-payload-searchitems.php

<?php
if ( !function_exists( 'get_paapi_payload_searchitems' ) ):
    function get_paapi_payload_searchitems($associate_tracking_id, $kw = '', $count = '10',$page = '1'){
        $payload="{"
            ." \"Keywords\": \"".$kw."\","
            ." \"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\""
            ." ],"
            ." \"ItemCount\": ".$count.","
            ." \"ItemPage\": ".$page.","
            ." \"PartnerTag\": \"".$associate_tracking_id."\","
            ." \"PartnerType\": \"Associates\","
            ." \"Marketplace\": \"www.amazon.co.jp\""
            ."}";
    return $payload;
    }
endif;

?>

main.php

<?php
require_once 'class.PAAPISearch
.php'; //PAAPI検索クラス
require_once 'class.AwsV4.php'; //AwsV4クラス
require_once 'paapi-payload-searchitems.php'; //PAAPI検索ペイロード

function get_amazon_search_html
($kw = '') {
    $search = new PAAPISearch;
    $search->kw = $kw; //検索キーワードをセット
    $search->get_response(); // レスポンスを取得
    $html = '';

    //レスポンスが取得できていた場合
    if(!empty($search->response)) {
        $paapi_response = json_decode($search->response); // オブジェクト形式にパース
        foreach ($paapi_response->SearchResult->Items as $item) {
            $html = '<dl>';
            $html .= '<dt>商品名</dt><dd><a href="'.$item->DetailPageURL.'">'.@$item->ItemInfo->Title->DisplayValue.'</a></dd>';
            $html .= '<dt>ASIN</dt><dd>'.@$item->ASIN.'</dd>';
            $html .= '<dt>JANコード</dt><dd>'.@$item->ItemInfo->ExternalIds->EANs->DisplayValues[0].'</dd>';
            $html .= '<dt>ISBNコード</dt><dd>'.@$item->ItemInfo->ExternalIds->ISBNs->DisplayValues[0].'</dd>';
            $html .= '<dt>価格</dt><dd>'.@$item->Offers->Listings[0]->Price->DisplayAmount.'</dd>';
            $html .= '</dl>';
        }
    }
    if(!empty($search->message)) { //メッセージが格納されていれば表示
        $html .= '<p>'.$search->message.'</p>';
    }
    return $html;
}

// 出力用のHTMLは適当に整形してください。
?>
<html>
<body>
<?php

echo get_amazon_search_html
('USBケーブル'); //引数にキーワードを渡すとAmazonでの検索結果が返ります。

?>
</body>
</html>

コメント

タイトルとURLをコピーしました