家電価格の定点観測を Google スプレッドシートで行う

  • 41
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

主にスクレイピングと定時実行を Google Apps Script で試しながら理解しようという記事です。

まえがき

※ プログラミング自体のお話はこの次のセクションからです。必要なければまえがきは読み飛ばしてください。

最近は、Google スプレッドシートに代表されるような Google ドライブがだいぶ浸透しました。業務にがっちり組み込まれた VBA アプリケーションやマクロなどがない場合は、MS Office からの移行も盛んなように思えます。

Excel もとい表計算ソフトが大好きな私達ですが、Google スプレッドシートもまた使い勝手が良いです。常時接続が当たり前となり、インターネットに接続していないと使えないというデメリットも取り沙汰されなくなりました。

Excel での VBA に対応するものが Google Apps Script (GAS) です。Google スプレッドシートを便利にすることができますが、もともとオンラインで前提のソフトウェアであることから、サーバのバッチプログラムといったような使い方ができるのも利点です。

この記事では、家電価格の定点観測をスクレイピングを使って行うことを通じて、そのような Google スプレッドシートの活用方法を見てみたいと思います。

詳しい解説は省略しているところがあります。Qiita の Google Apps Script のタグを見るといいかもしれません。

発想と準備

最近の私は引越しでの家電の買い直しのために家電量販店によく行っているのですが、よく製品の価格が上下する事に気づきました。明日来て買おうと思ったものが次の日に数万円値上げされて躊躇することもしばしば。

どういう価格の変動があるのかとか、自動でできるならそうしたほうがいいと思うのは自然な発想です。

今回はヨドバシカメラに行っていたのですが、ヨドバシカメラは店頭とウェブの価格がちゃんとリンクしています。大きな家電は実店舗で買いたいけれど、価格はウェブで…といったことに向いていますし、店頭の商品にQRコードが付いていてヨドバシアプリで読み取ればそのままヨドバシのネットショップサイトで買い物もできます。Amazon、価格.com の価格からは見劣りするかもしれませんが、ポイント還元を入れるとヨドバシカメラの価格は意外によかったりするし、Amazon より早く届く商品もあったりするので、使い分けは理にかなっているなと思います。

ヨドバシカメラは API を提供していない(はず)なので、まぁスクレイピングをするかとなります。LL で書いて Linux サーバで cron 実行をするというのも歴史ある良い作法ではあるのですが、今回は Google スプレッドシートを使ってみることにしました。なにせ、Google Apps Script の API にはスクレイピングに使える UrlFetchApp クラスといったものもあり、また時間主導型のトリガーもあったりしてcronのようなこともできるようなので、使ってみるのも面白いかなぁと思ったからです。もともとデータがスプレッドシート形式になっていることで、閲覧や解析やグラフ化なども楽だという利点もあります。

この記事で紹介するプログラムは、そう思い立って1時間ちょっとで雑に書いてみた内容をそのまま書きだしたものなので、GAS や JavaScript を専門にしている人からはツッコミどころがある解説になっているとは思いますが、ご了承ください。

詳細な解説は「Google Apps Scriptビギナーズガイド: Googleをプログラミングせよ! PRIMERシリーズ (libroブックス)」などが参考になると思います。

今回は

  • ヨドバシカメラのサイトから特定の商品のページをスクレイピングして、Google スプレッドシートの特定のシートに結果を一行追加する
  • 時間主導トリガーを使って、上記を行う関数を定期的に実行して商品価格の定点観測をする
  • 価格上下などの見やすさなど少し入れる

といった初歩的なことをやります。あまり欲張らず。

基礎は前述のサイトやタグや参考文献を参照していただくとして、簡単な流れとしては

  • 新規 Google スプレッドシートを作成
  • スプレッドシート内に「価格ウォッチ」シート(下のタブみたいなところ)を作成
  • 1行目は商品名の自由入力欄
  • 2行目には商品URLを入力
  • 3行目以降がプログラムで挿入する調査データが入る場所

というところまで進めます。

スクリーンショット 2015-09-21 0.27.27.png

A1 は「型番」という見出し名、A2 は「URL」という見出し名になっています。A3 以降のA列は、調査した日時文字列を入れることとして、その行が各製品URLをスクレイピングして得られた価格群が入るということにしましょう。

スクリーンキャプチャではスプレッドシート名が「引越しメモ〜」というタイトルになっていますが、シート名が決め打ちなだけで、スプレッドシート名は任意です。

スクリプト概要

アルゴリズム的には以下でしょう。

  • A1から下に走査して、最初に空いているA列のセルを探す。該当する行番号がnだとする。
  • An セルに現在の日時を入れる
  • B2 セルからURLを取得して、Bn セルにそこから得られる価格を入れる
  • C 列以降も同様(URLが得られるだけ横に行く)

スクレイピングについては UrlFetchApp クラスが全てです。API ドキュメントを読むと、だいたいの HTTP リクエストが出来ることがわかります。ここでは平凡な GET リクエストしか使わないので、使うメソッドは最小限です。

Google スプレッドシートの画面から [ツール] → [スクリプトエディタ] と選択してスクリプトエディタを起動します。新規作成時には何を作るか聞かれますが、単純に「空のプロジェクト」で良いでしょう。

スクレイピング

URL を GET して結果のボディを文字列として変数に入れられればいいことになります。

簡単な GET はこんな感じでできます。

var res = UrlFetchApp.fetch("http://www.yahoo.co.jp");
var body;
if ( res.getResponseCode() == 200 ) {
  Logger.log("正常");
  body = res.getContentText();
} else {
  Logger.log("異常");
}
if (body) 
  Logger.log("body: "+body);

これが分かれば後は試行錯誤でなんとかなります。

function yodobashiPrice (url) {
  var res = UrlFetchApp.fetch(url);
  var price, m;
  if ( res.getResponseCode() == 200 ) {
    m = res.getContentText().match(/id="js_scl_unitPrice".*?([\d,]+)/);
    price = m ? m[1].replace(/,/,"") : -1;
  } else {
    Logger.log("URL="+url+" fetch error.");
  }
  return price;
}

ヨドバシカメラの製品ページの HTML ソースコードを見て、パターンを把握します。

  • 2015/09/24 追加:マッチが失敗した場合(ページ構造が変わった場合)には例外とかにせず、欠損値として -1 を暫定的に入れています。

Google Apps Script には、与えられた文字列を XML としてパースして解析できる XmlService というクラスもありますが、ヨドバシから降ってきた HTML が整形式かどうかそもそもわからないでしょとか考えると、最初から割りきって正規表現パースしたほうが楽です。ページ構造が変われば、XMLあろうと正規表現であろうと修正が必要なんですから。

replace しているのは桁区切りのカンマを外しているだけです。とはいえこのままでは数字も文字列型であることに変わりはないので、もし後続で数値として計算したい場合には parseInt などを使ってやる必要があります。

日付文字列取得

ここでは調査実行日時を分かりやすい書式で入れたいと思います。

function pad (arg) {
  var s = arg.toString();
  return s.length == 1 ? "0" + s : s;
}

function today() {
  var now = new Date();
  var string = now.getFullYear().toString() + "-" + pad(now.getMonth()+1) + "-" + pad(now.getDate()) +
               " " + pad(now.getHours()) + ":" + pad(now.getMinutes());
  return string;
}

便利な日時表現ライブラリを使わないのであれば、上記のような感じで十分でしょう。printf 的な %02d を実現すべく pad という簡単な関数を定義しています。それを today 関数が使っているというだけです。JavaScript がわかれば Date の使い方はお馴染みなものでしょう。

空きセルを探す

シートを与えて、A列を上から下に向かって空きセルを探す関数は以下のような感じで大丈夫でしょう。

function _dateInsertionCell (sheet) {
  var i, cell;
  for ( i = 1 ; i < 1000 ; i++ ) {
    cell = sheet.getRange(i, 1);
    if ( cell.getValue().length === 0 ) break;
  }
  return cell;
}

無限ループ怖いってだけで while ではなく制限付きの for を使っています。

セルを cell として返せば、そのセルの行番号もセルの getRow メソッドから得られます。

本体

「価格ウォッチ」シートを得るところから、日時を入れる dateCell を見つけて日時を入れて、そのセルと同じ行を横に向かってスクレイピングしていくのは、上記関数を使ってこんな感じに書けます。

function walkWritePrice () {
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = spreadsheet.getSheetByName("価格ウォッチ");
  if ( !sheet ) return;
  var dateCell = _dateInsertionCell(sheet);
  dateCell.setValue(today());
  var urlColumnNumber = 2;
  var inputCell, url;
  for ( var i = 2 ; (url = sheet.getRange(urlColumnNumber, i).getValue()).length > 0 ; i++) {
    if ( i > 1000 ) break; // for safe
    inputCell = sheet.getRange(dateCell.getRow(), i );
    inputCell.setValue(yodobashiPrice(url));
    coloringCell(sheet, inputCell);
  }
}

function coloringCell (sheet, cell) {
  var upperCell = sheet.getRange(cell.getRow() - 1, cell.getColumn());
  if (!upperCell) return;
  var upprice  = parseInt(upperCell.getValue());
  var curprice = parseInt(cell.getValue());
  if ( upprice > curprice ) {
    cell.setBackground("yellow"); // 値下がり
  } else if ( upprice < curprice ) {
    cell.setBackground("red"); // 値上がり 
  }
}

バグか何かでループが止まらないのは嫌だなということで i が 1000 以上の場合はループをキャンセルしています。フェイルセーフ的ですね。

値を入力したら、そのセルが前回調査時より変化があったかは気になるところで、そのために色を付ける coloringCell 関数を入れています。価格の数値は文字列型のまま持ち越してきたので、ここで不等号比較をするのに parseInt しています。実際は数値データを取得直後に parseInt しておくべきケースが多いとは思いますが、一度セルに入れたデータを getValue した時に得られるデータが数値型なのか文字列型なのかわからなかったので、雑に割りきって書いています。

スクリプトエディタには上記関数を全部入れたものが完成形になります。スクリプトエディタの「関数を実行」から walkWritePrice 関数を選択して実行して、スプレッドシートの「価格ウォッチ」シートにデータが入るか試してみましょう。

この workWritePlace 関数を定期的に実行することで観測を行うことになりますが、 まずはメニューからこの関数を明示的に実行して動作がうまくいくか確認しましょう 。また、プログラムで新たな権限が必要な場合には許可ダイアログが出てきますので、明示的な実行でこれを事前に許可しておく必要があります。最初から時間主導型トリガーをしかけても、適切な権限を持っていなければエラーになってしまうからです。

時間主導型トリガー

うまく walkWritePrice 関数が実行できることを確認したら、それを時間主導型トリガーで定期的に実行するようにしてみましょう。

[リソース] → [現在のプロジェクトのトリガー] と選択をして、walkWritePrice 関数を定期的に実行する新しいトリガーを作成しましょう。

スクリーンショット 2015-09-21 1.21.12.png

あまり短い時間間隔に設定しても、Google に BAN される可能性もあったり、そもそも変化のないデータが大量に記録されて結局見づらかったりするので、せいぜい数時間間隔が妥当でしょう。

実行させていると価格上下の傾向が見えて面白いです。

スクリーンショット 2015-09-21 1.30.19.png

いくつかの知識を抑えるだけで、スクレイピングと時間実行を組み合わせたデータ収拾が簡単にできるのです。

注意点ですが、一般的にスクレイピングをする場合は長い時間をあけるなど迷惑がかからない常識的な範囲で行いましょう。

その他の拡張

Google Apps Script で色々な要望が叶えられます。

値上がり・値下がりがあったらすぐに知りたい

値上がりや値下がりはすぐに知りたい場合は、ケータイにメールを送るのがお手軽ではないでしょうか。

2つの価格が違う場合の条件分岐にメールを送信するコードを入れてしまいましょう。変更するのは coloringCell 関数、そして別途メールアドレスを入れた変数を用意します。

var mailto = "あなたのメールアドレス";

function coloringCell (sheet, cell) {
  // 一つ上のセルより価格が安かったら黄色にする
  var upperCell = sheet.getRange(cell.getRow() - 1, cell.getColumn());
  if (!upperCell) return;
  var upprice  = parseInt(upperCell.getValue());
  var curprice = parseInt(cell.getValue());
  var subject, body, kataban = sheet.getRange(1, cell.getColumn()).getValue();
  if ( upprice > curprice ) {
    cell.setBackground("yellow"); // 値下がり
    subject = "値下がりレポート";
    body = kataban + "が"+upprice.toString()+"円から"+curprice.toString()+"円に値下がりしました";
  } else if ( upprice < curprice ) {
    cell.setBackground("red"); // 値上がり 
    subject = "値上がりレポート";
    body = kataban + "が"+upprice.toString()+"円から"+curprice.toString()+"円に値上がりしました";
  }
  if ( mailto && subject && body ) {
    MailApp.sendEmail(mailto, subject, body) ;
  }
}

参考文献