【Node.js + Sheets API v4】Googleスプレッドシートを読み書きする

# はじめに
Node.jsでGoogleスプレッドシートを読み書きする。

使うもの：

* Node.js v10.16.0
* Google Sheets API v4
* Visual Studio Code



# Google Sheets API を使えるようにする

[Node.js Quickstart  |  Sheets API  |  Google Developers](https://developers.google.com/sheets/api/quickstart/nodejs)

上記サイトを参考に、まず Google Sheets API を使えるようにする。「Enable the Google Sheets API」のボタンを押してしばらく待つと、Google Cloud Platformに`Quickstart`という名前のプロジェクトが作成され、Client IDやシークレットと共に設定ファイルをダウンロードできるようになる。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424975/03a0be8a-62a0-24a6-f492-efce6615c5af.png)

↓クリックしてしばらく待った後の画面。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424975/bf2cbd96-cb93-fd5e-27c5-908a9a378516.png)

ダウンロードした`credentials.json`は、Node.jsプロジェクトのルートフォルダなど扱いやすい場所に置く。ダウンロードしそこねた場合は、Developer Console→APIとサービス→認証情報からダウンロードできる。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424975/203972be-a306-8084-e955-b5ed2d1a8502.png)

ソース管理しているフォルダに配置した場合は、`credentials.json`をソース管理対象外にして、うっかりコミットすることを防いでおく。

# ライブラリのインストール
VS Code のターミナル、またはWindowsコマンドウィンドウで以下を実行する。

```
npm install googleapis
```

# Visual Studio Code で readline を使えるようにする

Quickstartのサンプルでは、OAuthトークンをreadlineで受け取ろうとしている。しかし、Visual Studio Codeのコンソールは出力専用なので、VS Codeでスクリプトを実行すると入力の受け取りができない。このままではデバッグできないので、`launch.json`の設定を変え、コンソールを外部のものにする必要がある。

`launch.json`を開くには、メニューの「デバッグ」→「構成を開く」を選ぶ。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424975/cdfbe5da-4d6d-5fa3-f686-b7b47446e2c8.png)

すると`launch.json`が開くので、`configurations`の中に以下を追加する。

```
"console": "externalTerminal"
```

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424975/12146aae-494a-e04f-072e-43176d2e9b1b.png)

これでデバック実行時にコマンドプロンプトが起動するようになり、コンソール入力の受け取りが可能になる。

# サンプルの実行

Quickstartに載っているサンプルをコピーし、プロジェクトルートの`index.js`に貼り付ける。これでサンプルを実行できる状態になった。

↓実行できる状態のフォルダ
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424975/31382b71-cc33-078c-a492-69f323a6672f.png)

メニューの「デバッグ」→「デバッグの開始」で実行すると、Windowsの黒いコンソール画面が起動し、認証のためのURLが表示されるので、それをコピーしてブラウザに貼り付ける。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424975/e759b9ea-bb1d-8baf-0cf0-7ce8aa39b498.png)

Quickstartのアプリを許可するアカウントを選ぶ。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424975/7f580baa-5867-ef37-0462-294caa440f78.png)

Quickstartのアプリは先ほど作ったばかりでGoogleが安全と確認していない。そのためセキュリティ警告が表示されるが、詳細を表示して「Quickstartに移動」を選ぶ。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424975/406f3546-5ba5-156c-c788-4847724c107d.png)

この後、スプレッドシートの読み取りの許可を与えるかどうか確認されるので、「許可」を選択する。許可が終わると、アプリ側へ返すためのコードが表示されるので、これをコピーする。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424975/a8c0ed7d-8ded-10e7-7524-494348a2b914.png)

コピーしたコードをコンソールに貼り付けると、スプレッドシートの内容が読み取られてコンソールに表示される。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424975/48373155-1cfd-cb78-6930-35df514e6cc2.png)

これでQuickstartの実行は成功である。

ちなみに元データとなったスプレッドシートは、Googleがサンプルとして提供しているものを使っている。
https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit

# OAuth認証を得るまでのサンプル
Quickstartのものをそのまま使っても良いが、コールバック地獄が個人的に好きではないので、同期的にスクリプトを書けるようにしたものを以下に掲載する。`spreadsheet.js`の`init()`メソッドを呼び出せば、必要な場合にOAuthトークンをリクエストし、リクエスト済みであれば、保存したトークンを読み込むようにしている。

```javascript:spreadsheet.js
const fs = require("fs");
const readline = require("readline");
const {google} = require("googleapis");

const SCOPES = ["https://www.googleapis.com/auth/spreadsheets"];    // 読み書き可
const CREDENTIALS_PATH = "credentials.json";    // アプリ側の認証情報
const TOKEN_PATH = "token.json";    // トークン保存場所

let oAuth2Client = null;    // API実行に必要な認証情報

/**
 * モジュール初期化。
 * token.jsonがあればそれを読込み、無ければユーザーに認証を求める。
 */
module.exports.init = async function() {
    // 認証
    let credentialContent = fs.readFileSync(CREDENTIALS_PATH);
    let credentials = JSON.parse(credentialContent);
    oAuth2Client = new google.auth.OAuth2(
        credentials.installed.client_id
        , credentials.installed.client_secret
        , credentials.installed.redirect_uris[0]);
    
    if (!fs.existsSync(TOKEN_PATH)) {
        await getNewToken();
    }

    let tokenContent = fs.readFileSync(TOKEN_PATH);
    let token = JSON.parse(tokenContent);
    
    oAuth2Client.setCredentials(token);

};

/**
 * 新しいトークンを取得する
 */
async function getNewToken() {
    const authUrl = oAuth2Client.generateAuthUrl({
        access_type: "offline",
        scope: SCOPES});
    
    console.log("このURLへアクセスしてアプリを承認してください：", authUrl);

    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
    });

    return new Promise(function(resolve, reject) {
        rl.question("承認後に表示されたコードを入力してください：", async (code) => {
            rl.close();
            let {tokens} = await oAuth2Client.getToken(code);
            // tokenを保存する
            fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens));
            console.log("トークンを以下のファイルへ保存しました。", TOKEN_PATH);
        });
    
    });
}
```

# スプレッドシートIDの取得方法
読み書きするスプレッドシートは、スプレッドシートIDというもので特定する。
ブラウザでスプレッドシートを開いたときのURLの、`/d/`から`/edit`の間がスプレッドシートIDとなる。これをコピーしてスクリプトで使用する。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424975/8fc04802-4e16-131c-c808-781a4d1f792b.png)

# 範囲(range)の指定方法
基本的にExcelと同じ。

例：

* `Sheet1!A1:B2`
* `Sheet1!A:A` A列すべてのセル
* `Sheet1!1:2` 1～2行目のすべてのセル
* `Sheet1!A5:A` A5以降のA列すべてのセル
* `A1:B2` シート名を省略した場合、最初の可視なシートが対象になる。
* `Sheet1` Sheet1シートすべてのセル

# セルの取得
`sheets.spreadsheets.values.get()`を使う。

[Method: spreadsheets.values.get  |  Sheets API  |  Google Developers](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/get)

以下のスプレッドシートから取得する場合のサンプル。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424975/3d5eda07-3533-f305-2ca8-11fb54248c2e.png)

```javascript:spreadsheet.js(一部)

const {google} = require("googleapis");
const SPREADSHEET_ID = "xxxxx";

let oAuth2Client = null;    // API実行に必要な認証情報

module.exports.getSheetData = async function(){
    const sheets = google.sheets({version: "v4"});
    const param = {
        spreadsheetId: SPREADSHEET_ID,
        range: "シート1",
        auth : oAuth2Client
    };

    let response = await sheets.spreadsheets.values.get(param);
    let data = response.data;

    console.log(JSON.stringify(data));
}
```

↓実行結果(変数`data`の中身)

```json
{
	"range":"'シート1'!A1:Z1000"
	,"majorDimension":"ROWS"
	,"values":
		[
			["日付","商品","個数"]
			,["2019/12/1","りんご","10"]
			,["2019/12/2","バナナ","20"]
			,["2019/12/3","みかん","30"]
		]
}
```

データは2次元配列になっていて、日付も数値も文字列として取得される。

# セルの更新

## 単純な更新

[Method: spreadsheets.values.update  |  Sheets API  |  Google Developers](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/update)

↓サンプル

```javascript:spreadsheet.js(一部)
const {google} = require("googleapis");
const SPREADSHEET_ID = "xxxxx";

let oAuth2Client = null;    // API実行に必要な認証情報

module.exports.addData = async function(){
    const sheets = google.sheets({version: "v4"});
    const param = {
        spreadsheetId: SPREADSHEET_ID,
        range: "シート1!A5",
        valueInputOption: "USER_ENTERED",
        auth : oAuth2Client,
        resource : {
            values : [["2019/12/4", "もも", "40"]]
        }
    };

    await sheets.spreadsheets.values.update(param);
};
```

↓実行結果
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424975/b60dea3b-b1c3-b7bf-3c5e-3c1656559680.png)


## 表の末尾にデータを追加する

[Method: spreadsheets.values.append  |  Sheets API  |  Google Developers](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/append)

```javascript:spreadsheet.js(一部)
const {google} = require("googleapis");
const SPREADSHEET_ID = "xxxxx";

let oAuth2Client = null;    // API実行に必要な認証情報

module.exports.appendData = async function(){
    const sheets = google.sheets({version: "v4"});
    const param = {
        spreadsheetId: SPREADSHEET_ID,
        range: "シート1!A1", // 表を探索する場所を指定する。
        valueInputOption: "USER_ENTERED",
        insertDataOption : "INSERT_ROWS",
        auth : oAuth2Client,
        resource : {
            values : [["2019/12/4", "もも", "40"]]
        }
    };

    await sheets.spreadsheets.values.append(param);
};
```

実行結果は、A5にupdateした場合と同じ。

### InsertDataOption
新しいデータをどうやって挿入するかのオプション。

* `OVERWRITE` - 単純に既存データの末尾のセルに新しいデータを書き込む。
* `INSERT_ROWS` - 新しいデータを書き込む前に行が挿入される。そのため、既存データの位置がその分下がる。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424975/6e32ef62-f638-39b6-b568-df0fcc004feb.png)

## 既存の値を残しつつ上書きする
更新値にnullを指定した場合、その部分は既存データが残って上書きされない。

```javascript:spreadsheet.js(一部)

module.exports.overwrite = async function(){
    const sheets = google.sheets({version: "v4"});
    const param = {
        spreadsheetId: SPREADSHEET_ID,
        range: "シート1!A2",
        valueInputOption: "USER_ENTERED",
        auth : oAuth2Client,
        resource : {
            values : [[null, "ゴリラ", "99"]] // 1列目は上書きしない
        }
    };

    let response = await sheets.spreadsheets.values.update(param);
};
```
↓実行結果
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424975/2267b994-8369-914c-31b5-9eddc7f2ef1d.png)