RubyとPythonでGoogle APIを叩いてみた − データベースをSpreadsheetにしてGoogle Driveで管理する

# はじめに

日々たまっていくユーザーアクティビティ等のログデータ、もしくはユーザー登録データ（CSV）をGoogle Drive上にGoogleスプレッドシートの形式で保存する一連の流れを記録します。本メモを用いる事によって以下のことが可能になります。

- 英語文献しか充実していないGoogle APIの使い方を知ることができる
- RubyとPython両言語における仕様の違いを学ぶ事ができる
- 毎朝手作業でやっている仕事を自動化することにより時間の節約ができる
- 常にローカルで管理していたデータを効率的にクラウドで管理する術を得ることができる

等々、正直メリットしかありません。本メモのターゲットは「最短でとにかく作業自動化のやり方を知りたい人」および「Google APIを使ってみようとしたけど不親切な英語文献しか無くて挫折しかけている人」です。読了後にはそのような方々が**"どのような流れでGoogle APIを使うべきかなんとなく理解している"**状態をめざします。

_**※ 以下、RubyまたはPythonを使用できる環境が構築されていることを前提に話が進みます。**_

# 開発環境

#### 使用言語

- Ruby 2.2.2
- Python 2.7.10

#### OS

- OS X El Capitan 10.11.5

# 事前準備

まず初めにGoogle APIの利用登録が必要となります。以下の流れで実行してください。

1. **Google開発者コンソール**へ行く https://console.developers.google.com/start/api?id=drive
2. **"プロジェクトをつくる"** を選択、**ダッシュボード**が現れるまで続行する
3. プロジェクト作成後、**認証情報タブ**にある**"認証情報を作成"**を選択し**"OAuthクライアントID"**を選択
4. オプションからは**"その他"**を選択し、名前は適当な名前を入力し次の画面では"OK"をクリック
5. 認証情報をJSONとしてダウンロードし、`~/.credential/`配下に`client_secret.json`として保存
6. Gitを使用している人は `.gitignore` を編集し、APIキーを含まないように注意する

# ライブラリのインストール

#### Ruby
`$ gem install google-api-client`

#### Python
`$ pip install --upgrade google-api-python-client`

# スクリプトの作成

以下のスクリプトを任意のディレクトリに作成してください。また以下のRuby版とPython版が行っているタスクは100%同じではありませんのでご注意ください。

### Ruby: `uploader.rb`

```rb
require 'google/apis/drive_v3'
require 'googleauth'
require 'googleauth/stores/file_token_store'

require 'fileutils'

credentials_dir     = File.join(Dir.home, '.credentials')
CLIENT_SECRETS_PATH = File.join(credentials_dir, 'client_secret.json')
CREDENTIALS_PATH    = File.join(credentials_dir, 'sampleApp.yaml')
OOB_URI             = 'urn:ietf:wg:oauth:2.0:oob'
APPLICATION_NAME    = 'sampleApp'
SCOPE               = Google::Apis::DriveV3::AUTH_DRIVE

class DriveUploader
    # アップロードしたいCSVファイルが入っているフォルダへのパスを定義
    FILE_DIR = 'out/'

    # ファイルを保管しておきたいGoogle Drive上のフォルダのID等を定義
    # 詳しくは下でまとめて説明
    FOLDER_ID        = '<Your Folder ID>'
    MASTER_FILE_ID   = '<Your Master FIle ID>'
    MASTER_FOLDER_ID = '<Your Master Folder ID>'

    def initialize
        # APIインスタンスを生成
        @@drive   = Google::Apis::DriveV3
        @@service = @@drive::DriveService.new
        @@service.client_options.application_name = APPLICATION_NAME
        @@service.authorization = authorize
    end

    def authorize
        FileUtils.mkdir_p(File.dirname(CREDENTIALS_PATH))

        client_id = Google::Auth::ClientId.from_file(CLIENT_SECRETS_PATH)
        token_store = Google::Auth::Stores::FileTokenStore.new(file: CREDENTIALS_PATH)
        authorizer = Google::Auth::UserAuthorizer.new(client_id, SCOPE, token_store)
        user_id = 'default'
        credentials = authorizer.get_credentials(user_id)
        if credentials.nil?
            url = authorizer.get_authorization_url(base_url: OOB_URI)
            puts "Open the following URL in the browser and enter the " +
                 "resulting code after authorization"
            puts url
            code = gets
            credentials = authorizer.get_and_store_credentials_from_code(user_id: user_id, code: code, base_url: OOB_URI)
        end
        credentials
    end

    def upload_csvs
        ##
        # 実際にファイルをアップロードするメソッド
        # 指定フォルダに入っている"~~.csv"という名前のファイルを全てアップロードする

        # アップロードすべきファイルを探す
        file_path = File.join(FILE_DIR, '*.csv')
        files     = Dir.glob(file_path)
        abort('No files to upload.') if files.empty?

        # アップロードタスクを全ての該当ファイルに対して繰り返す
        counter = 0
        files.each.with_index(1) do |a_file, i|
            # ファイルの名前を"MMDD_FileName"というフォーマットに変換
            file_name = Date.today.strftime('%m%d') + a_file.gsub(/out\/\d{4}-\d{2}-\d{2}/,'').gsub('.csv','')
            puts "\nUploading[#{i}/#{files.count}]: #{file_name}"

            # アップロードする先を定義
            file_metadata = @@drive::File.new(
                name:      file_name,
                mime_type: 'application/vnd.google-apps.spreadsheet',
                parents:   [FOLDER_ID]
            )

            # Goodle Drive上の指定のフォルダに変換済みファイルをアップロードする
            file = @@service.create_file(file_metadata, upload_source: a_file, content_type: 'text/csv', fields: 'id')
            file_id = file.id

            puts "Successfully uploaded as:\nhttps://docs.google.com/spreadsheets/d/#{file_id}\n"
            counter += 1
        end

        # 結果を出力
        puts "\n\nTotal: #{counter.to_s} Files Uploaded\n"
    end

    def copy_master
        ##
        #アップロードと同時にGoogle Drive上でコピーしておきたい日報等マスターファイルをコピー
        #マスターファイルは既に定数として冒頭で定義してある通り

        master_file_metadata = @@drive::File.new(
            name:    "#{Date.today.strftime('%m%d')}_Dashboard",
            parents: [MASTER_FOLDER_ID]
        )
        master_file = @@service.copy_file(MASTER_FILE_ID, master_file_metadata)
        puts "\nSuccessfully created as: #{master_file.name}\nhttps://docs.google.com/spreadsheets/d/#{master_file.id}"
    end
end

# 本スクリプトをターミナルから [./uploader.rb] というコマンドで実行するための記述
if __FILE__ == $0
    puts "Start uploading reports to Drive..."
    DriveUploader.new.upload_csvs
    puts "Copying master file in Drive..."
    DriveUploader.new.copy_master
end
```

### Python: `uploader.py`

```py
import httplib2
import os
import sys
import time
import glob

from   apiclient      import discovery
import oauth2client
from   oauth2client   import client
from   oauth2client   import tools
from   apiclient.http import MediaFileUpload

CREDENTIAL_DIR     = os.path.join(os.path.expanduser('~'), '.credentials')
CLIENT_SECRET_FILE = os.path.join(CREDENTIAL_DIR, 'client_secret.json')
CREDENTIAL_PATH    = os.path.join(CREDENTIAL_DIR, 'piwikReport.json')
APPLICATION_NAME   = 'piwikReport'
SCOPES             = 'https://www.googleapis.com/auth/drive'

# アップロードしたいCSVファイルを保管しているフォルダまでのパスを定義
FILE_DIR = 'out/'

# ファイルを保管しておきたいGoogle Drive上のフォルダのID等を定義
# 詳しくは下でまとめて説明
FOLDER_ID        = '<Your Folder ID>'
MASTER_FILE_ID   = '<Your Master FIle ID>'
MASTER_FOLDER_ID = '<Your Master Folder ID>'

class DriveUploader(object):
    def __init__(self):
        """
        既にダウンロードして保存してある認証情報を使用して認証
        """
        self.credentials = self.get_credentials()
        self.http        = self.credentials.authorize(httplib2.Http())
        self.service     = discovery.build('drive', 'v3', http=self.http)

    def get_credentials(self):
        """
        既にAPI認証が済んでいないかどうか確認
        """
        if not os.path.exists(CREDENTIAL_DIR):
            os.makedirs(CREDENTIAL_DIR)

        store = oauth2client.file.Storage(CREDENTIAL_PATH)
        credentials = store.get()
        if not credentials or credentials.invalid:
            flags = tools.argparser.parse_args(args=[])
            flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES)
            flow.user_agent = APPLICATION_NAME
            if flags:
                credentials = tools.run_flow(flow, store, flags)
            else:
                # Python2.6を使用しているユーザーのための記述
                credentials = tools.run(flow, store)
            print('Storing credentials to' + CREDENTIAL_PATH)
        return credentials

    def upload_csvs(self):
        """
        実際にファイルをアップロードするメソッド
        指定フォルダに入っている"~~.csv"という名前のファイルを全てアップロードする
        """
        # アップロードするファイルを検索
        file_path = os.path.join(FILE_DIR, '*.csv')
        files     = glob.glob(file_path)
        if not files:
            print('No files to upload.')
            sys.exit()

        # 該当のファイル全てに対しファイル名を変更してアップロードするタスクを繰り返す
        counter = 1
        for a_file in files:
            # ファイルの名前を"MMDD_FileName"というフォーマットに変換
            file_name = time.strftime('%m%d') + a_file.replace('out/','_').replace('.csv','')
            print('>>>\nUploading[' + str(counter) + '/' + str(len(files)) + ']: ' + file_name)

            # アップロードする先を確定
            file_metadata = {
                'name'     : file_name,
                'mimeType' : 'application/vnd.google-apps.spreadsheet',
                'parents'  : [FOLDER_ID]
            }

            # 既に定義済みのGoogle Driveフォルダへスプレッドシートの形式に変換したCSVデータをアップロード
            media   = MediaFileUpload(a_file, mimetype='text/csv', resumable=True)
            file    = self.service.files().create(body=file_metadata, media_body=media, fields='id').execute()
            file_id = file.get('id')

            print('Successfully uploaded as:\nhttps://docs.google.com/spreadsheets/d/' + file_id)
            counter += 1

        # 結果を出力
        print('--------------\nTotal: '+ str(counter - 1) + ' Files Uploaded')

    def copy_master(self):
        """
        アップロードと同時にGoogle Drive上でコピーしておきたい日報等マスターファイルをコピー
        マスターファイルは既に定数として冒頭で定義してある通り
        """
        master_file_metadata = {
            'name'    : (time.strftime('%m%d') + '_PiwikReport'),
            'parents' : [MASTER_FOLDER_ID]
        }
        master_file = self.service.files().copy(fileId=MASTER_FILE_ID, body=master_file_metadata, fields='id, name').execute()
        print('Successfully created as: ' + master_file.get('name') + '\nhttps://docs.google.com/spreadsheets/d/' + master_file.get('id'))

# 本スクリプトをターミナルから [python3 uploader.py] というコマンドで実行するための記述
if __name__ == "__main__":
    DriveUploader().copy_master()
```

続いて、コード中の定数の説明をしておきます。

- `CREDENTIAL_DIR`
    - ダウンロードした認証情報が保管してあるローカルのフォルダまでのパス
- `CLIENT_SECRETS_PATH`
    - ダウンロードした認証情報ファイルまでのパス
- `CREDENTIALS_PATH`
    - 一度API認証が成功した後の認証情報を記録するファイルのパス
- `OOB_URI`
    - API認証に必要な情報
- `APPLICATION_NAME`
    - ファイルまたはアプリケーションの名前
- `SCOPE`
    - APIがアカウントにアクセスできる範囲の設定
- `FILE_DIR`
    - アップロードしたいCSVファイルが保存してあるローカルのフォルダまでのパス
- `FOLDER_ID`
    - CSVデータをスプレッドシート形式にしてアップロードしたいフォルダのID
    - IDとはそのフォルダのURLの最後尾にある英数字の羅列
    - フォルダのURLが `https://drive.google.com/drive/u/0/folders/7hu9HyWxhuga563WV2xxxxGd`　だったら、最後尾の `7hu9HyWxhuga563WV2xxxxGd` の部分
- `MASTER_FILE_ID`
    - CSVデータをアップロードすると同時にGoogle Drive上でコピーしたい日報などマスターファイルのID
    - IDとはそのファイルのURLの最後尾にある英数字の羅列
    - ファイルのURLが `https://docs.google.com/spreadsheets/d/1vm5eyjJpuXXXhogefjduaJGeoSQSjJfs2FBpiyoHige/edit`　だったら、最後尾直前の `1vm5eyjJpuXXXhogefjduaJGeoSQSjJfs2FBpiyoHige` の部分
- `MASTER_FOLDER_ID`
    - 日報などマスターファイルをコピーした後保管しておきたいフォルダのID
    - IDとはそのフォルダのURLの最後尾にある英数字の羅列
    - フォルダのURLが `https://drive.google.com/drive/u/0/folders/7hu9HyWxhuga563WV2xxxxGd`　だったら、最後尾の `7hu9HyWxhuga563WV2xxxxGd` の部分

# スクリプトの実行

上記スクリプトと同一のディレクトリに移動していることを確認した上で、以下のコマンドを実行してください。

#### Ruby
`$ ruby uploader.rb` または `$ ./uploader.rb`

#### Python
`$ python3 uploader.py`

初回は「認証しますか？」といった画面が出ることがありますが、全て許可してしまって構いません。どうしても気になるようでしたら、後から設定の中でいつでも変えることができます。またローカルのCSVファイルはアップロードが終わり次第消してしまいましょう。

# まとめ

以上です。本メモはGoogle APIを活用した作業自動化やクラウド化を標榜している方々に送る最速攻略記事でした。そもそもの経緯として海外でユーザーデータアナリストをしていたときに強要され、英語かつ不親切なドキュメントを前にヘロヘロになりながらやった体験があります。英語がトクイな方は以下に挙げる参考サイトを巡ってみてもよいでしょう。世の中の非効率的かつ非生産的な作業を強いられている方々の一人でも多くが、本記事を通して幸せになることを願って止みません。

<br>

---

###### 参考リンク一覧:

- Ruby
    1. [Ruby Quickstart](https://developers.google.com/drive/v3/web/quickstart/ruby)
    2. [REST Reference](https://developers.google.com/drive/v3/reference/)
    3. [API Explorer](https://developers.google.com/apis-explorer/#p/drive/v3/)
    4. [Method List](http://www.rubydoc.info/github/google/google-api-ruby-client/Google/Apis/DriveV3/DriveService)

- Python
    1. [Python Quickstart](https://developers.google.com/drive/v3/web/quickstart/python)
    2. [Getting Started](https://developers.google.com/api-client-library/python/start/get_started#setup)
    3. [Upload Files](https://developers.google.com/drive/v3/web/manage-uploads#exp-backoff)
    4. [API Explorer](https://developers.google.com/apis-explorer/#p/drive/v3/)