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

技術のメモ帳

気が向いたときに書いてます

[GAS]特定キーワードの人気レシピを解析する

Google Apps Script

今回は、cookpadの人気レシピを探すスクリプトを書いてみます。

スクレイピングによる情報収集においては、以下のマナーをご覧のうえ、お試しください。

仕様は次の通りです。

  1. Spreadsheetのメニューから、スクリプトを実行するUIを構築(キーワードの入力を受け付ける)
  2. 入力されたキーワードの、cookpadのレシピ検索結果からタイトルとURLを取得し、Spreadsheetに出力
  3. URLに対する外部サイトの評価を反映する(自作関数をSpreadsheetに埋め込む)

では、実装していきます。

SpreadsheetのメニューにUIを追加

SpreadsheetのUIにカスタムメニューを追加します。

スクリプト]→[スクレイピング実行]で実行できるようにします。

function onOpen() {
  var ui = SpreadsheetApp.getUi();
  ui.createMenu('スクリプト').addItem('スクレイピング実行', 'menuItem').addToUi();
}

function onEdit() {
  onOpen();
}

function menuItem() {
  var ui       = SpreadsheetApp.getUi(),
      response = ui.prompt('キーワードを入力:');

 if (response.getSelectedButton() == ui.Button.OK) {
   var keyword = response.getResponseText(),
       ss      = SpreadsheetApp.getActiveSpreadsheet(),
       sheet   = ss.insertSheet(keyword, 0),
       ssid    = ss.getId();
   new scrapeCookpad({ keyword: keyword, ssid: ssid }).run();
   Browser.msgBox('完了しました');
 } else {
   Browser.msgBox('キャンセルしました');
 }
}

実行すると次のようになります。

カスタムメニューと入力UIができました。

スクレイピング処理

HTMLを取得して、正規表現で必要な箇所を取得します。

UrlFetchApp.fetch()を連続実行すると、取得先のサーバに負荷がかかるため、1〜3秒ほどsleepさせます。

function scrapeCookpad(params) {
  this.host     = 'http://cookpad.com';
  this.keyword  = params.keyword;
  this.header   = ['TITLE', 'URL', 'TOTAL', 'facebook', 'google', 'b.hatena', 'twitter'];
  this.pattern  = { recipe: /(<a class=\"recipe-title.+?<\/a>)/gi, nextPage: /<a rel=\"next\" class=\"next\".+?<\/a>/i };
  this.limit    = 5;
  this.waitTime = { min: 1000, max: 3000 };
  this.ss       = SpreadsheetApp.openById(params.ssid);
  this.sheet    = this.ss.getActiveSheet();
  this.bookmark = {};
  this.share    = {};
}

scrapeCookpad.prototype = {
  run: function() {
    this.sheet.appendRow(this.header);
    this.setRecipes();
  },
  getPath: function(page) {
    return this.host+'/search/'+encodeURIComponent(this.keyword+'?order=date&page='+page);
  },
  setRecipes: function() {
    var page = 1;
    while (page <= this.limit) {
      var response = UrlFetchApp.fetch(this.getPath(page), { muteHttpExceptions: true }),
          content  = response.getContentText(),
          matches  = content.match(this.pattern.recipe),
          wait     = (function(min, max) { return Math.floor( Math.random() * (max - min + 1) ) + min })(this.waitTime.min, this.waitTime.max);
      Utilities.sleep(wait);
      for (var i in matches) {
        var elem  = XmlService.parse(matches[i]).getRootElement(),
            title = elem.getValue(),
            url   = elem.getAttribute('href').getValue(),
            row   = this.sheet.getLastRow() + 1;
        if (!/^(https?|ftp)(:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+)$/.test(url)) continue;
        var sum       = (function(row) { return ['=sum($D', row, ':$G', row, ')'].join('') })(row),
            funcShare = function(row, media) { return ['=getShareCount($B', row, ', "', media, '")'].join('') },
            facebook  = funcShare(row, 'facebook'),
            google    = funcShare(row, 'google'),
            twitter   = funcShare(row, 'twitter'),
            hatena    = (function(row) { return ['=getBookmarkCount($B', row, ')'].join('') })(row);
        this.sheet.appendRow([title, url, sum, facebook, google, hatena, twitter]);
      }
      if (this.pattern.nextPage.test(content)) page++; else break;
    }
  }
}

外部サイトの評価を取得する関数

リソース取得時に処理すると、リクエスト量が多くなるため、Spreadsheetの関数にしています。

評価を得る外部サイトは、Twitter, Facebook, Google, はてなブックマークにしました。

同一URLへのリクエストについては、キャッシュするようにしました。

function getBookmarkCount(url) {
  try {
    var cache  = CacheService.getScriptCache(),
        key    = 'http://api.b.st-hatena.com/entry.count?url=' + encodeURIComponent(url),
        cached = cache.get(key);
    if (cached != null) return Number(cached);
    var response = UrlFetchApp.fetch(key, { muteHttpExceptions: true }),
        value    = (response != '') ? response : 0;
    cache.put(key, value, 1500);
    return Number(value);
  } catch(ex) {
    return ex.toString();
  }
}

function getShareCount(url, media) {
  try {
    var cache  = CacheService.getScriptCache(),
        apiUrl = 'https://count.donreach.com/?url=' + encodeURIComponent(url),
        key    = apiUrl + '_' + media,
        cached = cache.get(key);
    if (cached != null) return Number(cached);
    var response = UrlFetchApp.fetch(apiUrl, { muteHttpExceptions: true }),
        contents = JSON.parse(response),
        value    = contents.shares[media];
    cache.put(key, value, 1500);
    return Number(value);
  } catch(ex) {
    return ex.toString();
  }
}

動作確認

スクリプトを実行すると次のようになります。

今回、実装途中で気付いたのですが、すべて1回の処理で行おうとすると、リクエストの回数を含めて無理が生じるので、こちらのサンプルは、あくまで練習用としてお考えください。

なお、これらのTIPSの個別エントリもありますので、ご興味がある場合はご覧ください。