Amazonアソシエイトの画像リンクが使えなくなったので、代替システムを作った話

当サイトはプライバシーポリシーページへの記載がある通り、Amazonアソシエイトプログラムに参加しており、そのシステムを利用して広告を掲載していました。

しかし、レポートを見る限りは2023年末に今まで掲載していたiframeを用いた画像付きリンクが機能しなくなりました。公式の告知によると、2023年11月をもって画像リンクの作成サービスが終了したとのことです。

なので、この代替システムを検討しなければならなかったのですが、私が利用しているWordpressのプラグインを利用すれば見栄えも良く素晴らしいリンクが作成できたりします。ただ、個人的に多くのプラグインを入れたくないのと、プラグインを用いると以前のような気軽さが大きく薄れる場合が多いです。プラグインを開き、どう見えるかを設定して…等と、平たく言ってしまえば面倒なのです。

そこで、いっそのこと自分でシステムを作ろうというのが今回の話です。今までAmazonアソシエイトのツールバーで画像リンクを使っていた人への代わりを探している人への答えの一つにはなると思います。まあ、システムと言いましたがそんなに大層なものではありません…

最初にこの記事でどんなものを作るかというと、ショートコードによって以下のような画像を含むリンクを作成できるようにします。

というわけでコードを含めてどういう構成にするのかという話を書きます。Wordpressを例にとって説明していますが、内容をしっかり読んでもらえれば他のCMSでも使えると思います。

目次

以前の広告の構成と使い方を考える

現在は使えなくなったアソシエイトの画像リンクの使い方を見てみましょう。そして私はWordpressを使っているのでそれを例に含めるとします。

  1. Amazonのサイトで紹介した製品のページを開く
  2. 画面上部にある「画像とテキスト」のボタンを押す
  3. iframeを含むコードが発行されるのでそれをコピーする
  4. WordPress側のカスタムHTMLのブロックに埋める

このようなステップで今まで画像リンクを作成していました。後はもう誰も使えないコードを載せておくとこんな感じでした。

<iframe style="width:120px;height:240px;" marginwidth="0" marginheight="0"
 scrolling="no" frameborder="0" 
src="https://rcm-fe.amazon-adsystem.com/e/cm?ref=qf_sp_asin_til&t=<userid>&m=amazon&o=9&p=8&l=as1&IS1=1&detail=1&asins=B08PFJSVMY&linkId=c640ffa9ae5922c99fc33a63a590ed86&bc1=ffffff<1=_top&fc1=333333&lc1=0066c0&bg1=ffffff&f=ifr">
    </iframe>

iframeによって構成されていました。ぶっちゃけWordpressだとこれが面倒で私が使っているテーマだと処理を変更しないとセンタリングできなかったりしました。

とはいえ、コピペでやることが完結していたので、実際に使う側としては非常に楽だったんですよね。だからこそこれを好んで使っていたわけです。サーバーのスペック的な部分や、高速に処理したいという意味で、他のプラグインを入れたくないのもありました。

これが今となっては使えなくなってしまったので、今画像付きリンクを作成しようと思うと、何かしらプラグインを使ったり、自分でリンクのみのテキストを作り画像を用意して綺麗に整形して…といった面倒な手順を踏まなければなりません

個人的に面倒を避けるという意味でも、自分の持つ最小限の機能を持たせるという意味でも、いっそのこと自分でプラグインなりコードを作製してしまおうということになりました。

同じようなシステムにするにはどうするか

以前のようにできる限り単純にするにはどうするかを考えます。それと実装面という現実的な面もよく考えてシステムを考えなければいけません。

そもそも、Amazonのサイトから色々やっていたのでそこをどうにかしないと依然と同じシステムを作らなければなりませんが、そうなるとChromeのプラグインかなんか作って何かしらしないといけないことになります。面倒な上にメンテナンスのことを考えると個人でやるには良い方法とは言えません。

そうなると現実的な方法はリンクをコピー、Wordpress側である入力フォームにそれを入れたら勝手にいい感じにしてくれるようにするという形でしょうか。

現実的な問題として、どうやってリンクから画像を含むデータを用意するのかという話で、それを調べていて出てきたのがPA-APIの存在です。実際にはこの方法は高速で実現できなそうなので、PA-APIとの兼ね合いを考えて、ASINというコードをページからコピーしてこちらでペーストする形に落ち着きます。

Product Advertising API 5.0(PA-API v5)の存在

Product Advertising API 5.0(PA-API v5、以下PA-API)というものがあるという話が先ほど出てきました。

そもそも、画像付きリンクを廃止したアソシエイト側はなぜ廃止したのかですが、端的に言ってしまうと、古いシステムを捨ててPA-APIに移行してもらいたいという状態だと思われます。以前使っていたiframeのものは長く運用されていたシステムでもありデザイン面からも古さを感じざるを得ないものでした。内部的なシステムもおそらく古いものだったのでしょう。

そこで、乗り換え先として出てくるのがPA-APIです。このAPIをたたくことによって何ができるのかという話なのですが、ざっくりと

  • 商品名の取得
  • 新品・中古の最安/最高価格の取得
  • 画像のサイズ別のリンクの取得
  • 商品の検索結果の情報

あたりを得ることができます。本当はもっと色々なことができるので、気になる人はPA-APIの詳細を見てみると良いでしょう。

結果的に、PA-APIでは名前から商品を検索したり、ASINからデータを取得できることがわかりました。当初考えていたAmazonのリンクに対して何か操作することは純正のAPIではできない状態です。

余談ですが、ASINとはAmazonが設定している各製品に一つずつ割り当てている固有の文字列のことです。製品ページの下側を探すとどこかに記載があります。

話を戻すと、以前のように特定の商品を紹介するためには、ASINを何かしらで得ることができれば、画像のリンクや商品名といった必要なものを取得できることがわかります。

しかし、ASINの取得はどうするのかを調べるとリンクからすぐに取得できない場合が多いようなので、製品ページから自分でコピーしてくるのが一番処理的にも早いことがわかりました。

ということで、実際に運用ではPA-APIを使ってASINを基に画像付きリンクを作成する方向に決めました。

PA-APIを使ったシステムの構成

今回はWordpressの場合を例にとって説明しますが、他のCMSで使う場合でも関数に対して引数を投げてその戻り値を利用できるようにできれば問題ないので、適宜変えて変更してもらえれば動くかと思います。実際の内部の流れを以下に示します。

  1. まずWordpressでの使い方はショートコードを使い、引数としてASINを投げて、関数を実行することでAPIをたたく
  2. コールバックから必要なデータを抽出
  3. データをhtml上で使える形に整形する
  4. 関数の戻り値として整形したデータを返す
  5. 結果、最初に紹介したような画像付きリンクが生成される

という流れになります。余談というかWodpress側の仕様でretunの値は、そのままhtml上に記述されるようなので戻り値としてコードを返すことで使えるデータの形にします。

実際に使う場合は、ショートコードとして以下のコードを書くだけになります。

これだけなので、非常に楽です。わざわざキーボード打つのですらしんどい場合は、プラグインとしてそういうブロックを作ってあげればいいのですが、そこらへんは私がやったことがなく、調べて書く気が起きなかったので実装しませんでした。

実際にWordpress上で実行するとこうなります。

代替システムとしては、作業としても楽に、十分現実的な形に落とし込んだのではないかと思います。

余談ですが、ショートコード形式にした一つの理由としてシークレットキーなどの情報が漏れないようにブラックボックス化できる点が挙げられます。実装で少し考えたのが、隠すべき部分をどう隠ぺいするかの部分でしたが、楽に解決できるのでこの形での実装となりました。

実際のコードと使い方

事前にAmazonアソシエイトのサイトにアクセスして、ツール⇒Product Advertising API より、「認証キーの管理」で、「認証キーの追加」を行います。その際のアクセスキーとシークレットキーを保存します。後で使うのと、同じものは再発行できないので忘れないようにデータにしておきましょう。

お決まりのことを書いておくと、コードの追加や操作を誤るとWordpress自体が起動しなくなったりするので、子テーマに追記を行うか、プラグインに以下のコードを格納してください。

ひとまず例として、function.phpに以下のようなコードを追加します。アクセスキーとシークレットキーをご自身の物に書き換えてご利用ください。

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 );

        $canonicalURL = $this->prepareCanonicalRequest ();
        $stringToSign = $this->prepareStringToSign ( $canonicalURL );
        $signature = $this->calculateSignature ( $stringToSign );
        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" );
    }
}

function get_paapi_info($asin) {
	
$response=get_transient("{$asin[0]}");
if($response===false){
$serviceName="ProductAdvertisingAPI";
$region="us-west-2";
$accessKey="アクセスキー";
$secretKey="シークレットキー";
$payload="{"
       ." \"ItemIds\": ["
        ."  \"{$asin[0]}\""
        ." ],"
        ." \"Resources\": ["
        ."  \"Images.Primary.Large\","
        ."  \"ItemInfo.Title\","
        ."  \"Offers.Summaries.LowestPrice\""
        ." ],"
        ." \"PartnerTag\": \"thunsuke-22\","
        ." \"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 );
$response = @stream_get_contents ( $fp );

$cache_time=60*60*(24+rand(0,9))*30;//秒数でキャッシュに時間を決め
set_transient("{$asin[0]}",$response,$cache_time);
}
$json_dec = json_decode($response);
$price=$json_dec->ItemsResult->Items[0]->Offers->Summaries[0]->LowestPrice->DisplayAmount;
$condition=$json_dec->ItemsResult->Items[0]->Offers->Summaries[0]->Condition->Value;
	if($condition=="Used"){//Summaries0がUsedだったときの分岐
		$price=$json_dec->ItemsResult->Items[0]->Offers->Summaries[1]->LowestPrice->DisplayAmount;
		$condition=$json_dec->ItemsResult->Items[0]->Offers->Summaries[1]->Condition->Value;
	}
$text="<div style=\"text-align:center;\" ><div class=\"paapi5-pa-ad-unit pull-left\"><div class=\"paapi5-pa-product-container\"><div class=\"paapi5-pa-product-image\"><div class=\"paapi5-pa-product-image-wrapper\"><a class=\"paapi5-pa-product-image-link\" href=\"{$json_dec->ItemsResult->Items[0]->Images->Primary->Large->URL}\" title=\"{$json_dec->ItemsResult->Items[0]->ItemInfo->Title->DisplayValue}\" target=\"_blank\"><img class=\"paapi5-pa-product-image-source\" src=\"{$json_dec->ItemsResult->Items[0]->Images->Primary->Large->URL}\" alt=\"{$json_dec->ItemsResult->Items[0]->ItemInfo->Title->DisplayValue}\"></a></div></div><div class=\"paapi5-pa-product-details\"><div class=\"paapi5-pa-product-title\"><a class=\"paap5-pa-product-title-link\" href=\"{$json_dec->ItemsResult->Items[0]->DetailPageURL}\" title=\"{$json_dec->ItemsResult->Items[0]->ItemInfo->Title->DisplayValue}\" target=\"_blank\">{$json_dec->ItemsResult->Items[0]->ItemInfo->Title->DisplayValue}</a></div><div class=\"paapi5-pa-product-list-price\"><p>{$price}</p><span class=\"paapi5-pa-product-list-price-value\"></span></div><div class=\"paapi5-pa-product-prime-icon\"><span class=\"icon-prime-all\"></span></div></div></div></div><style>.paapi5-pa-ad-unit {border: 1px solid #eee;margin:2px;position: relative;overflow: hidden;padding: 22px 20px;line-height: 1.1em;}.paapi5-pa-ad-unit * {box-sizing: content-box;box-shadow: none;font-family: Arial, Helvetica, sans-serif;    margin: 0;outline: 0;padding: 0;}.paapi5-pa-ad-unit a {box-shadow: none !important;}.paapi5-pa-ad-unit a:hover {color: #c45500;}.paapi5-pa-product-container{width: 100%;height: 210px;}.paapi5-pa-product-image {display: table;width: 150px;height: 150px;margin: 0 auto;text-align: center;}.paapi5-pa-product-image-wrapper {display: table-cell;vertical-align: middle;}.paapi5-pa-product-image-link {position: relative;display: inline-block;vertical-align: middle;}.paapi5-pa-product-image-source {max-width: 150px;max-height: 150px;vertical-align: bottom;}.paapi5-pa-percent-off {display: block;width: 32px;height: 25px; padding-top: 8px;position: absolute;top: -16px;right: -16px;color: #ffffff;font-size: 12px;text-align: center;-webkit-border-radius: 50%;-moz-border-radius: 50%;-ms-border-radius: 50%;border-radius: 50%;background-color: #a50200;background-image: -webkit-linear-gradient(top, #cb0400, #a50200);background-image: linear-gradient(to bottom, #cb0400, #a50200);}.paapi5-pa-ad-unit.hide-percent-off-badge .paapi5-pa-percent-off {display: none;}.paapi5-pa-product-details {display: inline-block;max-width: 100%;margin-top: 11px;text-align: center;width: 100%;}.paapi5-pa-ad-unit .paapi5-pa-product-title a {display: block;width: 100%;overflow: hidden;white-space: nowrap;text-overflow: ellipsis;font-size: 13px;color: #0066c0;text-decoration: none;margin-bottom: 3px;}.paapi5-pa-ad-unit .paapi5-pa-product-title a:hover {text-decoration: underline;color: #c45500;}.paapi5-pa-ad-unit.no-truncate .paapi5-pa-product-title a {text-overflow: initial;white-space: initial;}.paapi5-pa-product-offer-price {font-size: 13px;color: #111111;}.paapi5-pa-product-offer-price-value {color: #AB1700;font-weight: bold;font-size: 1.1em;margin-right: 3px;}.paapi5-pa-product-list-price {font-size: 13px;color: #565656;}.paapi5-pa-product-list-price-value {text-decoration: line-through;font-size: 0.99em;}.paapi5-pa-product-prime-icon .icon-prime-all {    background: url(\"https://images-na.ssl-images-amazon.com/images/G/01/AUIClients/AmazonUIBaseCSS-sprite_2x_weblab_AUI_100106_T1-4e9f4ae74b1b576e5f55de370aae7aedaedf390d._V2_.png\") no-repeat;    display: inline-block;margin-top: -1px;vertical-align: middle;background-position: -192px -911px;background-size: 560px 938px;width: 52px;height: 15px;}.paapi5-pa-product-offer-price,.paapi5-pa-product-list-price,{}.paapi5-pa-product-prime-icon {display: inline-block;margin-right: 3px;}@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {.paapi5-pa-ad-unit .paapi5-pa-product-prime-icon .icon-prime-all {background: url(\"https://images-na.ssl-images-amazon.com/images/G/01/AUIClients/AmazonUIBaseCSS-sprite_2x_weblab_AUI_100106_T1-4e9f4ae74b1b576e5f55de370aae7aedaedf390d._V2_.png\") no-repeat;display: inline-block;    margin-top: -1px;vertical-align: middle;background-position: -192px -911px;background-size: 560px 938px;width: 52px; height: 15px;}}@media  screen and (max-width: 440px) {.paapi5-pa-ad-unit {float: none;width: 100%;}.paapi5-pa-product-container {margin: 0 auto;width: 100%;}.paapi5-pa-product-details {text-align: center;margin-top: 11px;}}</style></div>";
return $text;
}
function test($asin){
	$cache_time=60*60*(24+rand(0,9))*30;
	delete_transient("{$asin[0]}");
	return "{$cache_time}";
}
add_shortcode( 'af', 'get_paapi_info' );

面倒だったのと他人が触る前提でもないので、コードにコメントは殆ど付けていません。流れを大雑把に言ってしまうと、APIをたたいて整形する関数と、ショートコードを追加するような形になっています。ヘッダーの作成やAPIの叩き方は公式のものを参考にしています。また、PA-APIをたたくのに回数制限があるので、Transient APIを使ってキャッシュとしてデータを保存することで、PA-APIをたたく回数を減らしています。キャッシュ時間にもばらつきを持たせることで、初回以外は一斉にAPIを叩かないような構成になっています。

最終的なhtml上のコードは公式ツールのScratchpadで確認できるコードを参考に、価格を付けて、画像を押してもページに行けるように改良しました。

私の場合は色々な兼ね合いもあって自作プラグインを作ってそこに自分の関数を全部格納しています。子テーマを作る必要も無いですし、アップデートもしやすいので楽です。

前述の通り、これを使うにはWordpressのエディター上で、

と入力するだけです。

コードを見ての通りではありますが、そもそもWodpressのショートコードはある特定の関数を必要なら引数も渡して呼び出すだけなので、同じコードを他のCMSで実装して同じように呼び出すことができれば使えると思います。関数内のreturnで返しているのは文字列なので、これを記事に反映できれば良いだけです。

個人的にはブラウザに変なプラグインを入れたりすることもなく、十分実用的な程度に代替システムを組めたのではないかと思います。

まとめ

Amazonアソシエイトのツールバーにあった、画像リンクの廃止に伴って、PA-APIを利用した代替システムを用意しました。今まで使い勝手が良かったのもあって、それをなるべく損なわない形での実装に落とし込んだつもりです。デザインも以前のiframeの物よりは少し現代風になった気もします。

実際に私もこのコードの形で順次差し替えて利用していますが、問題が発生することもなく使えています。

そもそも、画像リンクを廃止してPA-APIの利用を促すまではいいのですが、PA-APIの利用自体は利用者の自己責任ということで、代替システムを用意しないAmazonに対して思うところはありますが、ひとまずそこまでの時間をかけずに代替システムを用意できたのでよしとします。

役にたった!という方は以下のリンクのものを購入したり、一緒に購入すると一部がこのブログの運営資金になります。この記事を見た時点で知っていると思うので是非購入してください(笑)。

雑感としては、ある意味自分のコードに置き換えたので、好きに形もいじれますし、アップデートもしやすくなったので今後はこの形で愛用していきたいと思います。ただもう少しAPIも使いやすくできなかったのかとか思ったりするんですよね。

以上となります。お読みいただきありがとうございました。

投稿日:
カテゴリー: TOOL

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です