RubyでGoogle Drive/スプレッドシートを扱う

[@koshi_life](https://twitter.com/koshi_life) です。

GoogleDrive上で教師データ作成のためのラベル付作業を行いたく、
Rubyでフォルダやスプレッドシートの扱い方を調べたので備忘します。


# 利用ライブラリ

Rubyだと Google製の公式ライブラリ [googleapis/google-api-ruby-client](https://github.com/googleapis/google-api-ruby-client) も提供されていますが、情報が比較的多く、シンプルで使いやすかったのを理由に [gimite/google-drive-ruby](https://github.com/gimite/google-drive-ruby) を選びました。

- Google製のライブラリは 以下、Quickstartで試しました。
  - [Sheets API Ruby Quickstart](https://developers.google.com/sheets/api/quickstart/ruby)
  - [Drive API Ruby Quickstart](https://developers.google.com/drive/api/v3/quickstart/ruby)

### 前提

- ruby: v2.6
- google-drive-ruby: v3.0.2

# 利用準備

- GCPの利用プロジェクトの APIコンソール画面で `Drive API`と `Google Sheets API` を有効化
- 認証キー(OAuth クライアント ID)を作成 [参考:Authorizationのページ](https://github.com/gimite/google-drive-ruby/blob/master/doc/authorization.md#command-line)
  - ドキュメントオーナーは個人にしたくなったので、コマンドラインでの実行ユーザも兼ねてGoogleユーザを作成。サービスアカウント認証も対応していたが、各ドキュメントのサービスアカウントに対してシェア設定が必要になるので `OAuth クライアント ID`での認証とした。

client_id, client_secret を手に入れたら、以下キーを持つjsonを作成する。

```config.json
{
    "client_id": "xxx.apps.googleusercontent.com",
    "client_secret": "xxx"
}
```

Gemファイルに以下、1行を追加して、`bundle install` でライブラリをインストールする。

```Gemfile
gem 'google_drive'
```

# 使い方

少しづつコードに継ぎ足しをしながら理解を深めます。

## Step1: 初回実行時に認可、リフレッシュトークンを取得

```hello1.rb
require 'google_drive'

session = GoogleDrive::Session.from_config('config.json')
```

２行のみ`GoogleDrive::Session`の初期化まで書きました。
実行してみます。


```bash
$ bundle exec ruby hello1.rb

1. Open this page:
https://accounts.google.com/o/oauth2/auth?access_type=offline&client_id=xxx.apps.googleusercontent.com&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=https://www.googleapis.com/auth/drive%20https://spreadsheets.google.com/feeds/

2. Enter the authorization code shown in the page: [★☆★☆★ここに貼り付け★☆★☆★]
```

ユーザ認証用のURLが表示されるのでブラウザで開き認可します。
<img width="800px" alt="auth-google.png" src="https://qiita-image-store.s3.amazonaws.com/0/51734/4069d981-1ce3-51f6-a5d7-300cead7b11a.png">
最後の画面のコードをコンソールに貼り付けてEnter押下で完了です。


実行後 `config.json` を見ると、
`scope` と `refresh_token` の項目が追加されていることがわかります。
スコープの内容は[こちら](https://developers.google.com/identity/protocols/googlescopes)参照。

```config.json
{
  "client_id": "xxx.apps.googleusercontent.com",
  "client_secret": "xxx",
  "scope": [
    "https://www.googleapis.com/auth/drive",
    "https://spreadsheets.google.com/feeds/"
  ],
  "refresh_token": "xxx"
}
```

２回目の実行からはブラウザ開いて認証＆認可は不要になります。
ライブラリ側でこのリフレッシュトークンを元にアクセストークンが作られ、APIアクセスしていると思われる。


## Step2: 何も考えずファイル一覧

一旦動いている感を得たいので、session 取得後に１行追加して実行してみる。

```hello2.rb
require 'google_drive'

session = GoogleDrive::Session.from_config('config.json')
session.files.each { |file| p file.title }
```


```bash
$ bundle exec ruby hello2.rb 
"a"
"b"
"c"
...
```

動いている感を得られました。


## Step3: サブフォルダを作ってみる

```hello3.rb
require 'google_drive'

session = GoogleDrive::Session.from_config('config.json')

# Google DriveでFolderを選択した時のURLの末尾のハッシュ値
# 例: https://drive.google.com/drive/folders/xxx_xxx_xxx
root_folder_id = 'xxx_xxx_xxx'
sub_folder_name = 'hello_sub_folder'

root_folder = session.collection_by_id(root_folder_id)
sub_folder = root_folder.create_subfolder(sub_folder_name)

puts "SUBFOLDER: #{sub_folder.human_url}"
```

```bash
$ bundle exec ruby hello3.rb 
SUBFOLDER: https://drive.google.com/drive/folders/yyy_yyy_yyy
```

表示されたURLを開くとフォルダが作られていることが確認できます。

## Step4: 任意のファイルを指定フォルダにアップする

アップロード対象のファイル `hello_upload.txt`
指定のフォルダに配置するところまでやってみます。

```bash
$ cat hello_upload.txt
Hello Google Drive API
```

```hello4.rb
require 'google_drive'

session = GoogleDrive::Session.from_config('config.json')

# Title 指定、Google Documentに変換しない でアップロード
file = session.upload_from_file('hello_upload.txt', 'hello.txt', convert: false)
puts "FILE: #{file.human_url}"

# サブフォルダへ配下に移動
sub_folder_id = 'yyy_yyy_yyy'
sub_folder = session.collection_by_id(sub_folder_id)
sub_folder.add(file)
```

`Content-type` 指定などもできるらしい。
詳細は [Session:upload_from_file](https://www.rubydoc.info/gems/google_drive/GoogleDrive%2FSession:upload_from_file) 参照

## Step5: スプレッドシートのテンプレートをコピーして値を入れる

よくあるデータ加工業務のパターン。
テンプレートのスプレッドシートを用意しておき、シートをコピーして値を入れるといった内容。

例として、クラスごとの身体測定結果を管理するデータシートを作る業務をイメージして
テンプレートを作りました。

- [テンプレートシート](https://docs.google.com/spreadsheets/d/1sdPMpwZFvZCuAFqeKoZOlLyf5Tfn_3cQL7sjZcqpvHk/edit?usp=sharing)
- [以下スクリプトで作成したシート 3年A組分](https://docs.google.com/spreadsheets/d/1NC4fdNXOjTXNi1ru4tNm236uhdS3v3BBD9pmR8weKns/edit?usp=sharing)
- [以下スクリプトで作成したシート 3年B組分](https://docs.google.com/spreadsheets/d/1ysZ_HNAq6kfkxBjbCghHUBWmT0-rWjEaSoBuicGAnBA/edit?usp=sharing)

|テンプレ|データ投入後|
|:--|:--|
|<img width="400" alt="Screen Shot 2019-04-07 at 0.37.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/51734/e868bfbe-13ef-bff1-38b7-0a0fcea82f8e.png">|<img width="400" alt="Screen Shot 2019-04-07 at 0.36.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/51734/3eac8dfb-06cf-3c43-68ac-5bd22b4057ad.png">|



```hello5.rb

# テンプレートデータシートから任意のシート名、任意フォルダにシートをコピー
def copy_sheets(sheets_name, template_sheets, root_folder, sub_folder)
  sheets = template_sheets.copy(sheets_name)
  sub_folder.add(sheets)
  # テンプレートファイルと同じフォルダにも新シートの参照が残るので削除しておく
  root_folder.remove(sheets)
  sheets
end

# シートのデータを更新
def update_sheets(sheets, rows)
  top = 10 # データ投入位置 top
  left = 2 # データ投入位置 left
  work_sheet = sheets.worksheets[0]
  work_sheet.update_cells(top, left, rows)
  work_sheet.save
end

# ランダムデータの作成
def generate_random_data
  rows = []
  (1..20).each do |index|
    sex = rand(1..2)
    height = rand(140..200)
    weight = rand(40..100)
    rows << [index, sex, height, weight]
  end
  rows
end

# テンプレートファイルの置いてあるフォルダ
root_folder_id = 'xxx_xxx_xxx'
root_folder = session.collection_by_id(root_folder_id)

# データを配置したいフォルダ
sub_folder_id = 'yyy_yyy_yyy'
sub_folder = session.collection_by_id(sub_folder_id)

# テンプレートシート
template_sheets_id = 'zzz_zzz_zzz'
template_sheets = session.spreadsheet_by_key(template_sheets_id)

# ３年A組のシート作成
sheets_3a = copy_sheets('2019_3年A組', template_sheets, root_folder, sub_folder)
data_3a = generate_random_data
update_sheets(sheets_3a, data_3a)

# ３年B組のシート作成
sheets_3b = copy_sheets('2019_3年B組', template_sheets, root_folder, sub_folder)
data_3b = generate_random_data
update_sheets(sheets_3b, data_3b)
```


# 参考

- OAuth理解
  - [一番分かりやすい OAuth の説明](https://qiita.com/TakahikoKawasaki/items/e37caf50776e00e733be)
  - [Google APIのAccess Tokenをお手軽に取得する](https://qiita.com/shin1ogawa/items/49a076f62e5f17f18fe5)
- Ruby x google-drive-ruby
  - [RubyでGoogle スプレッドシートを操作する](https://techblog.gmo-ap.jp/2018/12/04/ruby_google_sheets_drive/)
  - [RubyでGoogleスプレッドシートを読み込み・書き込みする](https://www.virment.com/ruby-google-drive-readwrite/)