Amazon Kindle

[PA-API5.0]Amazon APIでオリジナルのランキング一覧表示スクリプトを作る

更新日:

はじめに

[PA-API5.0]PHPで人気Kindleマンガ売れ筋ランキングの書籍を表示する方法」でPA-API(Product Advertising API)5.0を軽く触りました。今回はこれを利用してAmazonで人気の商品リンクを作成したいと思います。

できあがりはこんな感じになります。

オリジナルパーツ作成

[PA-API5.0]PHPで人気Kindleマンガ売れ筋ランキングの書籍を表示する方法」にてScratchpadを使いPHPで任意のNodeID(BrowseNodeID)のAmazonリンクのJSONを取得してくるスクリプトを作成しました。作成といってもほぼサンプルを動かしただけですが、これをベースに継続的に更新し続けるブログパーツ的なものを作ろうと思います。

ブログパーツ構成

データベースを使いだすとやや面倒なのでjavascriptを活用して単純に取得したAmazonリンクの内容をdocument.writeするスクリプトを作成しようと思います。

この場合注意が必要なのはランキングを更新した際にキャッシュを削除してもらわないと内容がアップデートされないということです。そのため、末尾に日付のパラメータをつけて別ファイル化のようにすれば日付が変わったときに再読み込みされるようにします。

更新されたJS、CSS、画像のみブラウザキャッシュを破棄して読み込ませる

CSSを更新してもキャッシュで反映されない?更新内容を確実に反映させる方法

Amazonランキング取得PHPスクリプト

前回作成したスクリプトを多少改良してAPIから取得してきたAmazonランキングの情報をdocument.writeするjavascriptを作成するように変更しました。

パラメータはすべて冒頭にまとめています。BroseNodeIdやソートの種類等をここで変更することができます。一応、BroseNodeIdとソート順はGETで変更できるようにもしています。
途中の「output」という関数がオリジナルに追加した個所でこの中でAPIから受け取ったJSONを解釈してjavascriptで保存しています。

amazon_ranking.php

<?php
//各種パラメータ
$browseNodeId = "6043004051"; // BrowseNodeID(デフォルト:Kindleコミック)
$page = 1;                    // ページ番号(原則固定)
$sortidx = 2;                 // ソートの種類(0:評価順、1:注目順、2:新着順など)
$path = "amazonrnk.js";       // スクリプト保存先(適宜変更する)

$accessKey="{{ 自分のアクセスキー }}";
$secretKey="{{ 自分のシークレットキー }}";
$tag = "{{ 自分のアソシエイトタグ }}";
$serviceName="ProductAdvertisingAPI";
$region="us-west-2";

// ノード取得
if(isset($_GET["nodeid"])) {
  $browseNodeId = $_GET["nodeid"];
}
// ソート順取得
if(isset($_GET["sort"])) {
  $sortidx = $_GET["sort"];
}
$sortarr = array(
  "AvgCustomerReviews",
  "Featured",
  "NewestArrivals",
  "Price:HighToLow",
  "Price:LowToHigh",
  "Relevance"
);
$sort = $sortarr[$sortidx];


// 結果出力関数
function output($response) {
  global $path;
  global $style;
  $results = json_decode($response,true);
  $items = $results["SearchResult"]["Items"];
  $text = "";
  foreach($items as $item) {
    $tmp = array();
    $tmp["title"] = $item["ItemInfo"]["Title"]["DisplayValue"];
    $tmp["url"] =$item["DetailPageURL"];
    $tmp["imgurl"] =$item["Images"]["Primary"]["Large"]["URL"];

    if($tmp["imgurl"]==null) {
      $img = "<span class='noimg'><span>No Image</span></span>";
    } else {
      $img = "<a href='".$tmp["url"]."' target='_blank'><img src='".$tmp["imgurl"]."' /></a>";
    }
    $link = "<a href='".$tmp["url"]."' target='_blank'>".$tmp["title"]."</a>";
    $text .= "<div class='amzn_box'><div class='amzn_img'>".$img."</div><div class='amzn_link'>".$link."</div></div>";
  }
  $text = "<div class='amzn_area clearfix'>".$text."</div>";
  $text = str_replace('"', "'", $text);
  $text = "(function(){ document.write(\"".$text."\");})();";
  $text = mb_convert_encoding($text,"utf-8","auto");
  file_put_contents($path, $text);
}

$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\": \"".$browseNodeId."\","
        ." \"SortBy\": \"".$sort."\","
        ." \"ItemPage\": ".$page.","
        ." \"PartnerTag\": \"".$tag."\","
        ." \"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;
output($response);


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" );
    }
}
?>

デザインの調整

CSSとjavascriptでデザインを整えます。画像の縦横のサイズによって位置を調整したいためjavascriptでもデザインをいじることにします。
CSSでは横幅のみを%で指定しました。

amznstyle.css

.clearfix::after {
   content: "";
   display: block;
   clear: both;
}
.amzn_area {
 width: 95%;
 positoin: relative;
}
.amzn_img span.noimg{
    background: #ccc;
    width: 100%;
    height: 100%;
    display: block;
}
.amzn_img span.noimg span {
    display: block;
    width: 100%;
    text-align: center;
    top: 33%;
    position: absolute;
    font-size: 130%;
    font-weight: bold;
}
.amzn_box {
 width: 20%;
 position: relative;
 float: left;
 margin: 0 0 10px;
}
.amzn_img { 
 overflow:hidden;
}
.amzn_img img {
    width:100%;
}
.amzn_img span {
 color: white;
 background: #ccc;
}
.amzn_link {
 font-size: 16px;
 line-height: 16px;
 height: 32px;
 margin: 5px ;
 overflow: hidden;
}
.amzn_link a{
    text-decoration: none;
    color: #333;
}

次に縦幅についてはjavascriptで位置を調整します。

amazonrnkstyle.js

(function(){
var initial_h = 0;
[].forEach.call(document.getElementsByClassName("amzn_box"),function(x){
 var w = x.clientWidth;
 var h = x.clientHeight;
 var img_h = initial_h;
 if(initial_h==0) {
 	initial_h = parseInt(w*4/3);
 	img_h = initial_h;
 }
 var box_h = img_h + 40;
 x.style.height = String(box_h)+"px";
 x.children[0].style.height = String(img_h)+"px";

 var img_element = x.children[0].children[0].children[0];
 var intervalId = setInterval( function () {
	if ( img_element.complete ) {
		var width = img_element.naturalWidth;
		var height = img_element.naturalHeight;
		
		// 画像が横に長い場合縦の位置を真ん中に移動させる
		if(width>height) {
			img_element.style.position = "absolute";
			var top = parseInt((img_h-w/width*height)/2);
			img_element.style.top = String(top)+"px";
		} else {
			var img_rh = parseInt(w/width*height);
			
			// 画像が縦にはみ出ている場合縦に縮める
			if(img_rh>img_h) {
				img_element.style.position = "absolute";
				img_element.style.height = String(img_h)+"px";
				img_element.style.width = String(parseInt(img_h/img_rh*w))+"px";
				img_element.style.left = String(parseInt((w-img_element.width)/2.0))+"px";
				
			// 画像の下に空白がある場合真ん中に移動させる
			} else {
				img_element.style.position = "absolute";
				img_element.style.top = String(parseInt((img_h-img_rh)/2))+"px"
			}
		}
		clearInterval( intervalId ) ;
	}
 }, 500 ) ;
});
})();

※まれに要素の高さが少数の変換でばらつくのではじめの値に統一しています。

ページへの貼り付け

これまで作成したjavascriptとCSSファイルを貼り付けます。以下はサンプルですが、実際は自分の配置したところのパスを記述してください。デザインを修正するスクリプトの順番に気を付けてください。

<link rel="stylesheet" type="text/css" href="/sample/kindle/amznstyle.css">
<script type="text/javascript" src="/sample/kindle/amazonrnk.js?<?php echo strtotime('now') ?>"></script>
<script type="text/javascript" src="/sample/kindle/amazonrnkstyle.js"></script>

wordpressの記事の中で使う場合は場合以下のショートコードを追加すると良いです。

function amazon_ranking($atts) {
	$css = '<link rel="stylesheet" type="text/css" href="/sample/kindle/amznstyle.css">';
	$res = '<script type="text/javascript" src="/sample/kindle/amazonrnk.js?'.strtotime('now').'"></script>';
	$sc = '<script type="text/javascript" src="/sample/kindle/amazonrnkstyle.js"></script>';
	return $css."\n".$res."\n".$sc."\n";
}
add_shortcode('amazon_ranking','amazon_ranking');

この場合に記事の中で表示するには「[amazon_ranking]」と記載すればOKです。

(参考)ノードIDの取得

他のカテゴリの商品ランキングのAmazonリンクを作成したい場合はそのNodeIDを見つけてくる必要があります。公式でそのNodeIDを明示的に公開しているようなページは無さそうなので、Amazonのカテゴリ一覧ページ「https://www.amazon.co.jp/gp/site-directory」などから自分の決めたカテゴリのURL文字列を参考にNodeIDを探します。具体的にはURLのGETパラメータに「node=」の部分の値をコピーして貼り付けます。

例えば、テレビゲームの場合のリンクは「https://www.amazon.co.jp/TV%E3%82%B2%E3%83%BC%E3%83%A0/b?ie=UTF8&node=637394&ref_=sd_allcat_tvgames」ですが、この太字の個所「637394」がNodeIdとなります。

カテゴリによってはnodeパラメータが無いものもある上、全てのカテゴリで確認したわけではないので使う前にはスクリプトが正しく動くか必ず確認してください。

定期実行による更新

だいたいのレンタルサーバーにはLinuxの定期実行用のCRONタブの機能が使えるようになっています。Windowsでいうとタスクスケジューラ―に相当します。先ほどのPHPファイルを定期実行させて常に最新のランキングになるようにします。時間は1日に数回程度で良いように思います。CRONの設定の仕方は各レンタルサーバーで異なるため省略します。VPSの場合は通常のLinuxでできる設定をやればOKです。

ここで注意したほうが良いのはPHPファイルの置き場で、外部からアクセスできないところに保存したほうが良いということです。.htaccessで制限するなど各自対策してください。その場合、PHPファイルの中の$pathの内容も変える必要があるので適宜修正してください

結果

再掲になりますが、こんな形になります。デザインは適宜カスタマイズしてください。

終わりに

PA-API 5.0を使ってオリジナルのAmazonランキング一覧を表示させることができました。いったん設定すればずっと表示し続けることができると思います。

Amazonリンク
参考:mementoo.info

-Amazon, Kindle

Copyright© めめんと , 2020 All Rights Reserved Powered by AFFINGER5.