読者です 読者をやめる 読者になる 読者になる

今日も夏休み

技術的なあれこれ

GASでPS4 Proが定価販売されたら通知するシステムを作る

GAS AdventCalendar

これはマイネットエンターテイメント Advent Calendar 5日目の記事です。

事の始まり

最近FF15の盛り上がりがすごい。早くやりたい。
PS4 ProはFF15の発売に合わせて買うつもりだった。
よーしFF15出るしAmazonで買っちゃうぞー

¥55,000

(つд⊂)ゴシゴシ→(;゚ Д゚)

え?定価は¥48,578(税込)なのに…
このままじゃみんなと一緒にFF15を楽しめない。

やっぱつれぇわ…

おのれ転売屋たち…貴様らからは決して買わん!

こうしてAmazonとの戦いが始まったのであった…

要件定義

  1. Amazonが定価で販売していたら通知したい
  2. わざわざサーバーは用意したくない
  3. 定期的、大体10分に一度くらいのスパンでチェックしたい

GASで実装

上3つを満たす選択肢としてGoogle App Script(以下GAS)で作ることにした。
GASにはUrlFetchApp.fetch()というGETリクエストをする関数がある。
とりあえずこれで目的のページのクローリング、スクレピングを試みた。

Product Advertising API

しかしAmazonはクローリングを禁止していてAPI経由の取得のみしか許されていない。
これが実に面倒くさい。

APIを叩くにはアクセスキーの取得とアソシエイト登録が必要だった。
Access Key IDとSecret Access Keyの取得 - Amazon Web サービス

これが済んだら次はAPIの仕様を把握する必要がある。
基本的に把握する必要があるのは次の2つのパラメータ

  • Operation
  • ResponseGroup

Operationは動作の指定をするパラメータ
カートに商品を追加したり商品情報や売り手の情報などほしい情報や行いたい動作を指定する。

ResponseGroupは動作の詳細を指定するパラメータ
商品情報はASINで探すのか、それともキーワードで検索するのかなどを定義する。

次にAPI用のURLを作る

試すにはアクセスキーの暗号化やタイムスタンプの挿入が必要だがここで勝手にやってくれる。
http://associates-amazon.s3.amazonaws.com/signed-requests/helper/index.html

これが暗号化前に用意したURL

http://ecs.amazonaws.jp/onca/xml?
Service=AWSECommerceService&
AssociateTag=<アソシエイトID>&
Operation=ItemLookup&
ResponseGroup=OfferFull&
ItemId=B01LRHPUZ4

ItemLookupは指定したIDの商品情報を返すOperation。
OfferFullは出品者情報を返すResponseGroup。

これで現在のPS4 Pro出品者情報を返してくれる。
APIから適切なxmlが返ってくるのを確認したら次はGASスクリプトを組む。

GASソースコード

とりあえず今回は

  • 新品を5万円以下で販売している出品者がいる
  • トップ画面の販売者がAmazon

このどちらかの場合はメールで通知してくれるスクリプトを作った。

// チェックに引っかかったらメールで通知する
function notification(){
  var to = '<通知先メールアドレス>';
  var title = '【通知】PS4が買い時!';
  var message = '早く買え';
  GmailApp.sendEmail(to, title, message);
}

// 指定ASINの出品状況をxml形式でAmazonから取得
function getOfferInfo(asin){
  var endPoint = "http://ecs.amazonaws.jp/onca/xml?";
  var param = {
    Service:"AWSECommerceService",
    AssociateTag:"<アソシエイトID>",
    Operation:"ItemLookup",
    ItemId:asin,
    Timestamp:new Date().toISOString(),
    AWSAccessKeyId:"<AWSAccessKeyId>",
    ResponseGroup:"OfferFull"
  };

  var urlParams = Object.keys(param).sort();
  urlParams = urlParams.map(function(key){
    return key +"="+encodeURIComponent(param[key]);
  });

  var addParams = "GET" + "\n" + "ecs.amazonaws.jp" + "\n" + "/onca/xml" + "\n" + urlParams.join("&");
  var secretKeyEncoded = Utilities.base64Encode(Utilities.computeHmacSha256Signature(addParams, "<AWSSecretKey>"));
  var url = endPoint + urlParams.join("&") + "&Signature=" + encodeURIComponent(secretKeyEncoded);
  var xml = UrlFetchApp.fetch(url);

  return xml.getContentText();
}

function main(){
  // PS4 ProのASIN
  var asin = "B01LRHPUZ4";
  var xml = getOfferInfo(asin);
  var document = XmlService.parse(xml);
  var root = document.getRootElement();
  var xmlns = 'http://webservices.amazon.com/AWSECommerceService/2011-08-01';
  var ecs = XmlService.getNamespace(xmlns);

  // 新品の最低価格
  var amount = root
    .getChild('Items', ecs)
    .getChild('Item', ecs)
    .getChild('OfferSummary', ecs)
    .getChild('LowestNewPrice', ecs)
    .getChild('Amount', ecs)
    .getText();

  // ページTOPの出品者
  var topMerchant = root
    .getChild('Items', ecs)
    .getChild('Item', ecs)
    .getChild('Offers', ecs)
    .getChild('Offer', ecs)
    .getChild('Merchant', ecs)
    .getChild('Name', ecs)
    .getText();

  // 新品の最低価格が50000円以下、またはAmazonが出品していたら通知
  if(amount < 50000 || topMerchant == 'Amazon.co.jp'){
    notification();
  }
  
  Logger.log("トップ出品者 : " + topMerchant);
  Logger.log("最低価格 :" + String(amount));

}

とりあえずこれを10分間隔で回してる。

詰まったポイント

  • ASINで検索しても引っかからない

(ASINはAmazon固有のID)
エンドポイントをhttp://webservices.amazon.com/にしていた
これはアメリカ用のエンドポイントで日本の商品は引っかかったりかからなかったり。
英語の記事見ながら作っていたのでそのまま使ってしまっていた。

  • XMLのパースが上手くいかない

確認できるのがGASのログだけなのでオブジェクトの状態がいまいち把握できなかった。
原因はgetChildren()で取得していたからであり、全てに[0]をつけなければいけなかった。
getChildren()は同じ名前のエレメントが複数あるときに使用するもので、
直下にあるエレメント名がユニークならgetChild()で取得すべきだった。

  • アマゾンが出品しているかどうかをAPIでは確認できなかった

アマゾンの在庫があればトップに出てくる販売者はアマゾンである確率が高いのでそれを確認。
それと妥協して新品で5万円以下で出品している販売者がいるときも通知の対象とした。