Twitter API と GAS を用いた情報収集 - 3. Google スプレッドシートとの連携 編

一連の記事では、TwitterAPI の情報を GAS 経由で取得し、Google スプレッドシートに録り溜めていく方法を、ステップバイステップで説明していきます。

[前回](https://qiita.com/anti-digital/items/acbd70b3ecedc6ff0b38) は、OAuth1 ライブラリを用いて、GAS から TwitterAPI の呼び出しを行う最小のコードを示しました。

今回は、GAS から Google スプレッドシートに書かれたキーワードを参照し、そのキーワードを使って TwitterAPI の Search を呼び出し、得られたツイートデータを Google スプレッドシートに書き戻す方法について説明します。

しかしこの辺りは、TwitterAPI へのアクセスを除いて、ごくごく一般的な GAS の処理だと思うので、**日々 GAS を書いてらっしゃる人からすると、新しい発見はこの記事にないかと思います。** 
…という点を踏まえて読んでいただけると助かります。

# Google スプレッドシートの準備

GAS は、Google スプレッドシートに内包されるものではなく、[前回](https://qiita.com/anti-digital/items/acbd70b3ecedc6ff0b38)用意した、スタンドアローンのものを拡張して使います。
ここでいう「内包されるもの」とは、Google スプレッドシート上で [Tools] ⇒ [Script Editor] で書き始められる GAS のことを言っています。

ちなみに、私は、次のように GAS を使い分けています。(完全に好みの問題です)

- 既存の Google スプレッドシート上にあるデータで、お手軽に、秒でなんらかの簡単な処理をさせてしまいたい場合は、内包する GAS
- 特定の Google スプレッドシートに依存しない、ある程度汎用性のある処理を作る場合はスタンドアローンの GAS

そういうわけで、Google スプレッドシートは別で用意し、ID を控えておきます。
ちなみに、ID は、Google スプレッドシートの URL の下記の場所になります。

`https://docs.google.com/spreadsheets/d/{Google Spreadsheet ID}/edit#gid=0`

# Google スプレッドシートの仕様決め

仕様はいくらでも複雑にできますが、一連の記事ではシンプルさを重視して、次のように決めます。

- A1 セルに検索用キーワードが書かれている
- A2 セルに前回のキーワード保存される
- 3 行目以降の行末に Search API で取得したツイートの情報が保存されていく
- GAS が走ったとき、A1 ≠ A2 なら、新しいキーワードが指定されたものとして 3 行目以降をクリアする

# 蛇足: Google スプレッドシートの読み込みと書き込みについて

C/C++ や C# で Excel にアクセスするプログラムを作ったことのある人なら、メモリの使用量を気にして 1 セルずつ処理することを試みたことがあると思います。しかしこの方法だと恐ろしく処理時間がかかり、使い物になりません。

そうではなく、読み込み/書き込みするのに必要な複数のセルを Range でがばっとメモリ上に取り、メモリ上で必要な処理をした後、Range で Excel に書き戻すようにすると、使用に耐えられる処理速度が出てきます。

Google スプレッドシートの処理も、Excel のそれと似ており、[**getRange()**](https://developers.google.com/apps-script/reference/spreadsheet/sheet?hl=en#getRange(Integer,Integer,Integer,Integer)) で範囲指定した後、[**getValues()**](https://developers.google.com/apps-script/reference/spreadsheet/range?hl=en#getvalues) で必要なデータを配列に取り込み、配列を処理した後、[**setValues()**](https://developers.google.com/apps-script/reference/spreadsheet/range?hl=en#setvaluesvalues) でシートに書き戻します。

Range は、左上のセル (A1) から、データが書かれている最大行([getLastRow()](https://developers.google.com/apps-script/reference/spreadsheet/sheet?hl=en#getlastrow))、最大列([getLastColumn()](https://developers.google.com/apps-script/reference/spreadsheet/sheet?hl=en#getlastcolumn))まで取ることが多いですが、Google スプレッドシートでは、[**getDataRange()**](https://developers.google.com/apps-script/reference/spreadsheet/sheet?hl=en#getdatarange) でそれを一発で取得できるなど、便利な関数が多数用意されています。

# コード

前回の最後のコードに、Google スプレッドシートの読み込み/書き込み処理を付け足します。
シートへ記載する Tweet データは、ユーザー名、Tweet した日付け、Tweet 内容の 3 つを、それぞれ、A, B, C 列に書き込むこととしました。

```JavaScript
let VAL_CONSUMER_API_KEY     = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
let VAL_CONSUMER_API_SECRET  = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
function logOAuthURL() {
  var twitterService = getTwitterService();
  Logger.log(twitterService.authorize());
}
function getTwitterService() {
  // Create a new service with the given name. The name will be used when
  // persisting the authorized token, so ensure it is unique within the
  // scope of the property store.
  return OAuth1.createService('twitter')
      // Set the endpoint URLs.
      .setAccessTokenUrl('https://api.twitter.com/oauth/access_token')
      .setRequestTokenUrl('https://api.twitter.com/oauth/request_token')
      .setAuthorizationUrl('https://api.twitter.com/oauth/authorize')
      // Set the consumer key and secret.
      .setConsumerKey(VAL_CONSUMER_API_KEY)
      .setConsumerSecret(VAL_CONSUMER_API_SECRET)
      // Set the name of the callback function in the script referenced
      // above that should be invoked to complete the OAuth flow.
      .setCallbackFunction('authCallback')
      // Set the property store where authorized tokens should be persisted.
      .setPropertyStore(PropertiesService.getUserProperties());
}
function resetTwitterService() {
    var twitterService = getTwitterService();
    twitterService.reset();
}
function authCallback(request) {
  var twitterService = getTwitterService();
  var isAuthorized = twitterService.handleCallback(request);
  if (isAuthorized) {
    return HtmlService.createHtmlOutput('Success! You can close this tab.');
  } else {
    return HtmlService.createHtmlOutput('Denied. You can close this tab');
  }
}
function twitterSearch(keyword, maxCount) {
    let encodedKeyword = encodeURIComponent(keyword);
    try {
        var twitterService = getTwitterService();
        if (!twitterService.hasAccess()) {
            Logger.log(twitterService.getLastError());
            return null;
        }
        var url = 'https://api.twitter.com/1.1/search/tweets.json?q='
            + encodedKeyword
            + '&result_type=recent&lang=ja&locale=ja&count='
            + maxCount;
        var response = twitterService.fetch(url, { method: "GET" });
        var json = JSON.parse(response);
        Logger.log(json);
        return json;
    }
    catch (ex) {
        Logger.log(ex)
        return null;
    }
}
// 古い main() はコメントアウト
//function main() {
//  twitterSearch("天気",50);
//}
// ⇓ 今回付け足したコード ⇓
function updateSheet(sheet) {

  let keywordNew = ''; // 今回のキーワード
  let keywordOld = ''; // 前回処理時のキーワード

  // セル A1 から、2 行 1 列分の範囲を取得
  let range = sheet.getRange(1, 1, 2, 1);
  // 取得した範囲の値を取得
  let values = range.getValues();

  // セル A1 にある今回のキーワードの取得
  keywordNew = values[0][0];
  // セル A2 にある前回のキーワードの取得
  keywordOld = values[1][0];
  // セル A2 にある前回のキーワードを今回のキーワードで更新しておく
  values[1][0] = keywordNew;

  // 2 行を一旦書き込む
  range.setValues(values);

  // 今回のキーワードが空、または、新規キーワードなら、3 行目以降の全セルをクリア
  if ( sheet.getLastRow() > 3 && (keywordNew == '' || keywordNew != keywordOld ) ) {
    range = sheet.getRange(3, 1, sheet.getLastRow()-2, sheet.getLastColumn());
    values = range.getValues();
    for ( let row=0; row<values.length; row++) {
      for ( let col=0; col<values[row].length; col++) {
        values[row][col] = '';
      }
    }
    // 一旦書き込む
    range.setValues(values);
  }

  // keyword が空でなければ Tweet データを取得してシートの更新
  if ( keywordNew != '') {
    let json = twitterSearch(keywordNew, 50);
    let countTweets = json.statuses.length;
    // 更新をする最初の行の決定
    let rowUpdateStart = ( keywordNew != keywordOld )? 2 : sheet.getLastRow();
    // Tweet 記入用のレンジの取得
    range = sheet.getRange(rowUpdateStart+1, 1, countTweets, 3);
    values = range.getValues();
    let row = 0, col = 0;
    json.statuses.forEach( tweet => {
      values[row][col++] = tweet.user.name;
      values[row][col++] = tweet.created_at;
      values[row][col] = tweet.text;
      row++; col=0;
    });
    range.setValues(values);
  }
}
let VAL_ID_TARGET_BOOK = 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'; // Google スプレッドシートの ID
function main() {
  // Google スプレッドシートの取得
  let book = SpreadsheetApp.openById(VAL_ID_TARGET_BOOK);
  // 含まれる全シートの取得
  let sheets = book.getSheets();
  // 全シートについて updateSheet() を呼んで処理をする
  sheets.forEach(function (sheet) {
    updateSheet( sheet );
  }); 
}

```

これで、Google スプレッドシートのセル A1 に「天気」などと記入し、**main()** を実行すると、シートに結果が記入されます。

<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1859956/99934d86-a45c-c1dd-3793-1259689a7551.png">

**updateSheet()** 関数で、検索キーワードの取得、前回のキーワードとの比較、データ消去、更新とうしているので、やや冗長で読みにくくなっているかもしれませんが、基本的には **getRange()** ⇒ **getValues()** でシートのデータを取得して、**setValues()** で書き戻すことをしています。

後はこれを用途に合わせて発展させていけば、ニーズに合った仕様のツールを得られると思います。

以上で一連の記事を締めくくりたいと思います。

---
* [Twitter API と GAS を用いた情報収集 - 1. Twitter Developer Account 編](https://qiita.com/anti-digital/items/5f085d8d7361785f7def)
* [Twitter API と GAS を用いた情報収集 - 2. GAS+OAuth 編](https://qiita.com/anti-digital/items/acbd70b3ecedc6ff0b38)
* [Twitter API と GAS を用いた情報収集 - 3. Google スプレッドシートとの連携 編](https://qiita.com/anti-digital/items/f7d6de42974066ad1f25)