新年あけましておめでとうございます。久しぶりの投稿です。
最近アウトプットを全然していないためか、ポートフォリオのスカスカ具合が気になり始めてきた今日この頃です。

さて、今回はGAS(Google App Script)の勉強として、ひとつ簡単なツールを作ってみました。
GASは結局Google Appsを操作するためのAPIですし、操作内容もイメージしやすいので、使うときに勉強すればいいか~と思っていましたが、どうやら巷にあふれるGAS案件をGETするにはそれなりのGAS経験が無いと厳しいようなので、無精せずに触っておきます。

いくつかの案件を見た感じ、最低限、GASを使って次のことができるようになっておくと良さそうです。

  • Google Spreadsheetsの基本的操作
  • Google Spreadsheets上の何かしらのアクションをトリガーにしてGASを実行
  • 外部との通信(GASを使った外部データの取得)とデータの挿入、成形
  • 画像の表示

これに加えて、次のことができると良さそうですが、これらは課題としておきます。

  • Google Formの基本的操作
  • GASをWeb APIやWeb Pageとして公開
  • GASの長期実行とその管理
  • 巨大なデータをいい感じに管理するテク

ということで今回は、Twitterのトレンドとそれに関する人気の呟きをTwitter APIで取得し、それぞれ一覧として表示するツールを作ってみました。

成果物(.gs)は Gist にアップロードしてあります。

ツールの概要

初期状態は、mainシートとconfigシートの2つで構成されています。

mainシート

mainシートには”Run”ボタンと”Clear”ボタンが置かれています。
“Run”ボタンをクリックすることで、後ほど説明するlistシートとtweetsシートが生成されます。
“Clear”ボタンをクリックすることで、listシートとtweetsシートを削除します。”Run”ボタンクリック時の最初にも、この動作は行われます。

configシート

configシートでは、Twitter APIや表示に関する設定を管理します。

listシート(トレンドリスト)

listシートは、mainシートの”Run”ボタンクリック時に生成・更新され、Twitterのトレンドリストが表示されます。

tweetsシート(人気の呟き)

tweetsシートは、mainシートの”Run”ボタンクリック時に生成・更新され、Twitterのトレンドに関する人気の呟きが表示されます。
各呟きは、リツイート数といいね(favorite)数の合計により降順でソートされます。画像が存在する場合は、最初の画像のみが2列目に表示されます。

実装

シートによる設定の管理

ソースコードをGistで公開したかったのと、利用者的目線から考えて、設定はシートから読み取るようにしています。

var CONFIG_CELL_POS = {
  API_KEY: [2, 2],
  API_SECRET: [3, 2],
  ACCESS_TOKEN: [4, 2],
  ACCESS_TOKEN_SECRET: [5, 2],
  TREND_LIMIT: [6, 2],
  TWEET_LIMIT: [7, 2],
  WOEID: [8, 2],
  IMAGE_HEIGTH: [11, 2],
  IMAGE_WIDTH: [12, 2]
};

var _memoConfigSheet = null;
function getConfigSheet() {
  return _memoConfigSheet = _memoConfigSheet || getActiveSpreadsheet().getSheetByName(SHEET_NAMES.CONFIG);
}

function getConfig(key) {
  var sheet = getConfigSheet();
  var pos = CONFIG_CELL_POS[key];
  return sheet.getRange(pos[0], pos[1]).getValue();
}

Twitter APIの利用

OAuthはGoogle製のライブラリである googlesamples/apps-script-oauth1 を利用しています。
いちいちAccess tokenを発行するのは面倒なので、予めApplication Managementで発行しておいたのを使っています。

APIは、トレンドの取得である GET https://api.twitter.com/1.1/trends/place.json と、呟きの検索APIである GET https://api.twitter.com/1.1/search/tweets.json を使っています。

function getService() {
  return OAuth1.createService('Twitter')
    .setConsumerKey(getConfig('API_KEY'))
    .setConsumerSecret(getConfig('API_SECRET'))
    .setAccessToken(getConfig('ACCESS_TOKEN'), getConfig('ACCESS_TOKEN_SECRET'));
}

function fetchTrend() {
  var service = getService();
  var url = 'https://api.twitter.com/1.1/trends/place.json?id=' + getConfig('WOEID');
  var response = service.fetch(url);
  var result = JSON.parse(response.getContentText());
  return result[0].trends.slice(0, getConfig('TREND_LIMIT'));
}

function fetchPopularTweetList(keyword) {
  var service = getService();
  var url = 'https://api.twitter.com/1.1/search/tweets.json?q=' + encodeURIComponent(keyword) + '&result_type=popular&count=' + getConfig('TWEET_LIMIT');
  var response = service.fetch(url);
  var result = JSON.parse(response.getContentText());
  return result.statuses;
}

取得した情報をスプレッドシートに書き込む

テーブル情報をすべて書き込むという操作はよくありそうなので、今後も使えるよう、便利関数として writeTable() を定義しています。
画像の表示は Spreadsheet#insertImage() を使おうとしたのですが、この関数だと挿入した後の画像のリサイズ方法がわからなかったので、スプレッドシート自体の IMAGE() 関数を使って画像を読み込み、セルの大きさを変えることでリサイズしています。

function writeTable(sheet, header, body) {
  var cnum = header.length;
  var rnum = body.length;

  var ary = [header].concat(body);
  sheet.getRange(1, 1, rnum + 1, cnum).setValues(ary);

  // resize columns
  for (var i = 0; i < cnum; ++i) {
    sheet.autoResizeColumn(i + 1);
  }
}

function getFirstMediaUrl(tweet) {
  if (!tweet.entities.media || !tweet.entities.media.length) { return null; }
  var media = tweet.entities.media[0];
  return media.media_url_https || media.media_url;
}

function updateTrendList(sheet, trends) {
  var header = ['name', 'volume', 'url'];
  var body = trends.map(function (trend) {
  return [
    trend.name,
    trend.tweet_volume,
    trend.url
  ];
  });
  writeTable(sheet, header, body);
}

function updateTweetList(sheet, tweets) {
  var header = ['keyword', 'photo', 'user', 'retweets', 'favorites', 'text', 'url'];
  var body = tweets.map(function (tweet) {
    var mediaUrl = getFirstMediaUrl(tweet);
    return [
      tweet.keyword,
      mediaUrl ? '=IMAGE("' + mediaUrl + '")' : null,
      tweet.user.name + '(@' + tweet.user.screen_name + ')',
      tweet.retweet_count,
      tweet.favorite_count,
      tweet.text,
      'https://twitter.com/' + tweet.user.screen_name + '/status/' + tweet.id_str
    ];
  });

  writeTable(sheet, header, body);

  // resize image
  sheet.setColumnWidth(header.indexOf('photo') + 1, getConfig('IMAGE_WIDTH'));
  tweets.forEach(function (tweet, i) {
    if (getFirstMediaUrl(tweet)) {
      sheet.setRowHeight(i + 2, getConfig('IMAGE_HEIGTH'));
    }
  });
}

その他

ActiveSpreadsheetの取得はメモ化しています(何ns変わるんだろう)。

var _memoSpreadSheet = null;
function getActiveSpreadsheet() {
  return _memoSpreadSheet = _memoSpreadSheet || SpreadsheetApp.getActiveSpreadsheet();
}

シートのリセットは削除により対応しています。
Sheet#clear() というのもあるのですが、セルの幅や高さがリセットされないので、毎回作り直すことにしました。
clearSheets() はスプレッドシートから呼び出す用です。

function clearSheetsByNames(sheetNames) {
  var ss = getActiveSpreadsheet();
  sheetNames
    .map(function (name) { return ss.getSheetByName(name); })
    .filter(function (sheet) { return sheet; })
    .forEach(function (sheet) { ss.deleteSheet(sheet); });
}

function clearSheets() {
  clearSheetsByNames([SHEET_NAMES.LIST, SHEET_NAMES.TWEETS]);
}

findOrCreateなんちゃら はDB関連のライブラリでよくありますね。欲しかったで作りました。

function findOrCreateSheetByName(ss, name) {
  return ss.getSheetByName(name) || ss.insertSheet(name);
}

main

main() はmainシートの”Run”ボタンに紐づけている関数です。

function main() {
  var ss = getActiveSpreadsheet();

  // fetch and update trend list
  var listSheet = findOrCreateSheetByName(ss, SHEET_NAMES.LIST);
  var trends = fetchTrend();
  updateTrendList(listSheet, trends);

  // fetch and update several popular tweets
  var allTweets = [];
  trends.forEach(function (trend) {
    var tweets = fetchPopularTweetList(trend.name);
    tweets.forEach(function (tweet) {
      tweet.score = tweet.retweet_count + tweet.favorite_count;
      tweet.keyword = trend.name;
      allTweets.push(tweet);
    });
  });

  // desc order
  allTweets.sort(function (a, b) { return b.score - a.score; });

  var tweetsSheet = findOrCreateSheetByName(ss, SHEET_NAMES.TWEETS);
  updateTweetList(tweetsSheet, allTweets);
}

反省

  • listシートって命名センスが酷いですね。何もわからない。trendsシートにすればよかったです。
  • 脳死でmain関数なんて作りましたが、もっとまともな名前を付ければよかったです。
  • writeTableのbodyは各header名をkeyにしたオブジェクトの配列にした方がいい感じがします(どっちも欲しい)。
  • ライブラリの設定が若干面倒なので、OAuthを自前実装したかったのですが、HMAC-SHA-1の実装がバグって時間が溶けたので止めました(googlesamples/apps-script-oauth1のソースコードは短くて読みやすいので、これを読んで実装すればそんなに難しくないはずなんですが…)。

感想

もっと早くGASに手を出しておけばよかったと思います。学習コストが低いわりに、できることがかなり増えるように感じます。趣味でも使っていきたいです。

ところで、久しぶりに旧石器時代のJavaScriptを書きました。なかなかしんどいです。しかも、モジュール分割が結構めんどいらしく、それなりに大きいものを作ろうとすると辛いコードになりそうです。
トランスパイルしてアップロードする方法はいろんな記事で紹介されていますが、受注案件の場合はそれをすると顧客が嫌な顔をする気がします。
そのうちGASがECMA2015に対応することを祈っています。

2件のコメント

  1. GASとTwitterAPIを使ってトレンド情報を集めたかったのでとても参考になりました。
    しかし、上記のコードだとSHEET_NAMESという配列が書かれていないので
    自分で下のコードを追加する必要がありました。
    var SHEET_NAMES = {
    CONFIG : “config”,
    LIST : “list”,
    TWEETS : “tweets”
    }

    返信

コメントを残す

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

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください