炭火Blog

Amazon PA-API v5.0を利用した商品紹介リンクを作る

※このページにはプロモーションが含まれています。

  1. [最重要]アソシエイト・プログラム・ポリシーを確認する
  2. Amazon API 5.0 スクラッチパッドを使って雛形を取得
  3. コードの使い方
  4. コードの説明
  5. 実装を見送ったもの
  6. 実装を予定しているもの

WordPressで使うテーマを自作できるようになり、もう少し難しいことに挑むシリーズ。第2弾はAmazon Products Advertising APIを利用したアマゾン社の販売商品のリンクを作ります。

前回のGoogle Analyticsを使ったコードと同じように、今回も雛形となるコードを取得できます。今の私の力では、何もないところから作るのは難しく、元になるコードから改変できるものを探しています。

骨格があれば、なんとか調べて自分の作りたい、理想のものに近づけるだけです。

WordPressは追加したい機能があればプラグインを追加することを推奨しています。しかし、プラグインを追加すると、プラグインのPHP・CSS・場合によってはJavaScriptまで追加され、ページの読み込み速度に大きなマイナスの要因になってしまいます。

自分でPHPのコードとCSSを追加すれば、必要な機能だけを追加可能で、ページの読み込み速度に悪影響がとても少なく済みます。

このコードはどんなエラーが出るか分かりません。それなりの期間使用していおり、今の所問題は無いようですが、このコードで生じた不具合や、損失には責任が持てませんのでご了承ください。

[最重要]アソシエイト・プログラム・ポリシーを確認する

せっかくアマゾンアソシエイトプログラムの参加資格を得ても規約違反を行ってしまっては、最悪の場合は参加資格を失ってしまいます。

この投稿を公開する前に規約を読んでコードを作成したのですが、公開した後にアソシエイト・プログラム・ポリシーを再確認すると、規約に変更があり、投稿の内容が現在の規約に反している箇所があったので、一度非公開にしました。

APIを利用するに当たり特に注意する箇所は「アソシエイト・プログラムIPライセンス」「アソシエイト・プログラム商標ガイドライン」です。当然に、それ以外の箇所も遵守が求められますので熟読されることをおすすめします。

アソシエイト・プログラム・ポリシー

気になる部分を上げてみましたが、特に価格情報を記載する場合に1時間以内に表示を更新しないといけない部分は、キャッシュのコントロールができないと難しいなと思います。

Amazonセールとイベント

Amazon API 5.0 スクラッチパッドを使って雛形を取得

Amazon Products Advertising API 5.0 Scratchpad

スクラッチパッドのページから左の「GetItems」を選び、1番上の選択肢からamazon.co.jpを選び、下のリストの3つと商品のAsinを入力します。

次に「Resources」プルダウンの中に下のリストと同じものを探しチェックを入れます。

黄色い「Run request」を押すと雛形のコードを取得できます。

AmazonアソシエイトのアカウントがあるだけではAPIは動作しないので注意が必要です。売上実績がない状態ではAPIは使えません。また、売上の額が少ないとAPIの使用可能回数も少ないです。

スクラッチパッドで取得できたコードを元に、PHPの本を片手にしてコードに変更を加えました。

コードの使い方

[amazon asin="Amazon商品のasin"]

表示したい箇所に上のショートコードを記述することでAmazonの商品を紹介するリンクが表示されます。

そして、下のコードはfunctions.phpに記述してください

<?php 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;

function amazon_product_info_shortcode($atts) {
    // パラメータから ASIN コードを取得
    $asin = isset($atts['asin']) ? sanitize_text_field($atts['asin']) : '';

    if (empty($asin)) {
        return 'ASIN コードが指定されていません。';
    }

    $cache_key = 'amazon_product_' . $asin;
    // キャッシュから商品情報を取得
    $product_info = get_transient($cache_key);

    if (false === $product_info) {
        // キャッシュが見つからない場合、APIを呼び出して商品情報を取得
        $product_info = fetch_amazon_product_info($asin);

        // 商品情報をキャッシュに保存(1日間有効)
        set_transient($cache_key, $product_info, 1 * DAY_IN_SECONDS - (rand(0, 120) * 60));
    }

       // 商品情報が存在するか確認
       if ($product_info) {
        // 商品情報からHTMLを生成
        $html = generate_product_html($product_info);
        return $html;
    } else {
        return '商品情報が見つかりませんでした。';
    }
}

function fetch_amazon_product_info($asin) {

    $serviceName = "ProductAdvertisingAPI";
    $region = "us-west-2";
    $accessKey = "アクセスキー";
    $secretKey = "シークレットキー";
    $payload = "{"
            . " \"ItemIds\": ["
            . "  \"$asin\""
            . " ],"
            . " \"Resources\": ["
            . "  \"Images.Primary.Medium\","
            . "  \"Images.Primary.Large\","
            . "  \"ItemInfo.ByLineInfo\","
            . "  \"ItemInfo.Title\""
            . " ],"
            . " \"PartnerTag\": \"XXX-XXX-22(トラッキングID)\","
            . " \"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) {
        error_log('Failed to open stream for Amazon API.');
        return null;
    }
    $response = @stream_get_contents($fp);
    if ($response === false) {
        error_log('Failed to get API response.');
        return null;
    }
 
    $product_data = json_decode($response, true);

    if ($product_data && isset($product_data['ItemsResult']['Items'][0])) {
        $product_info = $product_data['ItemsResult']['Items'][0];
    } else {
        // エラーが発生した場合の処理
        error_log('Failed to get product info from API response.');
        return null;
    }
    
    return $product_info;

}
    
function generate_product_html($product_info) {
   
    // 商品情報からHTMLを生成
    $asin = $product_info['ASIN'];

     // タイトルを取得
     if (isset($product_info['Title']) && isset($product_info['Title']['DisplayValue'])) {
        $title = $product_info['Title']['DisplayValue'];
    } else {
        // タイトル情報が存在しない場合、デフォルトタイトルを使用
        $title = $product_info['ItemInfo']['Title']['DisplayValue'];
    }

    $manufacturerFields = ['Manufacturer', 'Brand', 'Publisher', 'Director', 'Creator', 'Actor', 'Artist', 'Author'];
    $manufacturer = '';
    
    if (isset($product_info['ItemInfo']['ByLineInfo']['Contributors'][0]['Name'])) {
        $manufacturer = $product_info['ItemInfo']['ByLineInfo']['Contributors'][0]['Name'];
    
        if (isset($product_info['ItemInfo']['ByLineInfo']['Contributors'][0]['Role'])) {
            $manufacturer .= ' ' . $product_info['ItemInfo']['ByLineInfo']['Contributors'][0]['Role'];
        }
    } else {
        foreach ($manufacturerFields as $field) {
            if (isset($product_info['ItemInfo']['ByLineInfo'][$field]['DisplayValue'])) {
                $manufacturer = $product_info['ItemInfo']['ByLineInfo'][$field]['DisplayValue'];
                break;
            }
        }
    }

        $image_url_m = $product_info['Images']['Primary']['Medium']['URL'];
        $image_url_l = $product_info['Images']['Primary']['Large']['URL'];
        $detail_page_url = $product_info['DetailPageURL'];

        $html = '<div><img src="'.esc_attr($image_url_m).'" srcset="'.esc_attr($image_url_m).' 1x,'.esc_attr($image_url_l).' 2x" alt="'.esc_attr($title).'" width="160" height="160" loading="lazy"></div>';
        $html .= '<div>'.esc_html($title).'</div>';
        $html .= '<div>'.esc_html($manufacturer).'</div>';
        $html .= '<div><a href="'.esc_url($detail_page_url).'" target="_blank" title="'.esc_attr($title).'" rel="sponsored noopener">Amazon</a></div>';
        return $html;
    }

add_shortcode('amazon', 'amazon_product_info_shortcode');

//キャッシュクリア
function clear_amazon_product_cache($asin) {
    $cache_key = 'amazon_product_' . $asin;
    delete_transient($cache_key);
}
$changed_asin = '';
clear_amazon_product_cache($changed_asin);

コードの説明

商品画像をRetinaとスマートフォンに対応

APIで取得できる画像の大きさは3種類あり、今回はそのうちの中と大の2種類を取得しています。

中の大きさが160px、大が500pxになり、数値は縦横の長い方になります。例えば中だと縦横160x100pxや50x160pxの大きさになります。htmlには「width=160 height=160」と書いているのでCSSで

img {
    object-fit: contain
}

と記述して画像が伸びないようにします。

160pxの画像は通常の解像度の場合に表示され、Retinaモニタやスマートフォンの場合は500pxの大きさの画像が表示されます。それによってどの環境でも商品画像がボケずにクリアに表示されます。

PageSpeed Insightsで適切なサイズの画像の指摘を受けることもありません。

本のデータに含まれる「著」「編集」等を表示する

APIで取得したJsonデータをいくつか見ると、本の場合は著者名とともに「著」「編集」などのデータが出力されています。

必要性は感じませんが、PHPをより使えるようになる為の作業なので、表示するようにしました。

データを1日間キャッシュする

限りある大事になAPIのリクエスト数です。できるだけ使わないほうが良いので、1度APIからデータを取得すると1日間キャッシュします。変更する場合は20行目の1を変えてください。

以前は価格情報などを含まない場合はキャッシュの期間に制限はなかったように思うのですが、改めてライセンスを読むと、24時間がキャッシュ期間の上限でした。

APIのリクエストが同じタイミングだとtoo many requestsエラーが出て、同一ページにある複数の商品リンクが取得できない恐れがあります。そこで、22時間から24時間の間に幅をもたせてキャッシュの期限を定めます。

キャッシュをクリアする方法

コードを作っていて困ったのが、作成途中に壊れたデータがキャッシュされることでした。コードを修正しても壊れたデータのキャッシュを読み込んでしまうと、コードの修正結果が分かりません。

コードの最後尾がキャッシュを削除するるコードです。クリアしたいデータのAsinを入れて、その商品が表示されるページにアクセスします。そうすることで、キャッシュがクリアされ、新たにデータを取得します。

実装を見送ったもの

当初は価格を表示させようとしてAmazonアソシエイトの利用規約を読んだところ、価格を表示する場合は価格が購入するまでの間に変更される可能性の注意書きの必要性と、キャッシュできる期間が1時間であることが分かりました。

注意書きはなんとでもなりますが、キャッシュを1時間にすると、当ブログのようなほとんど商品紹介リンクを利用されないサイトの場合は、すぐ利用上限に達してしまいます。

APIが使えなくなるとデータが取得できなくなり、リンクが表示できなくなります。そうなるのが目に見えているので実装を見送りました。

実装を予定しているもの

商品の名前をAsinと同時にショートコードで入力し、それを楽天市場とYahoo!ショッピングで検索用キーワードとして使い、結果を表示させようと思います。

現在は自分のページでテスト中なのでテストが終われば公開する予定です。

改めて規約を読んでみると、Amazon APIを利用した画像やタイトルに対して直接に他社のリンクを付けることは認められてない上に、画像やタイトル以外のリンクも認められていませんでした。

また、他社の商品を紹介する場合にAmazon社の商標が使えません。当然といえば当然ですが規約が変更になったことで、楽天とYahoo!のリンクは削除しました。

購入者のレーティングを星5つの形で表示させたいと思ったのですが、APIから出力されるJsonデータにレーティングが含まれていないので不可能でした。

最後までお読みくださり、誠にありがとうございます。

WordPressユーザーのためのPHP入門 はじめから、ていねいに。[第3版]
1冊ですべて身につくWordPress入門講座

関連投稿

週間アクセスランキング

管理者 ほんだ

数多くあるブログの中で、このページをお読みくださりありがとうございます。このブログは、炭火で美味しいものを作ることを中心に、日々の趣味についてを文章にすることで、WordPressを使ってのWebページ作成を忘れないようにしています。熱帯魚の世話や野菜の栽培、Linuxについて興味のあることを、つたない文章で綴っています。兵庫県在住です。

Home
Blogging
Amazon PA-API v5.0を利用した商品紹介リンクを作る