GoogleAppsScript と Python の連携(Python calls GAS Functions)

PythonからGAS(Google Apps Script)関数を呼び出すためのメモ。

目標

GAS関数を利用してGoogleAPIを一本化しよう!

下準備

読者がどれくらいGoogleサービスに慣れているかわからないが、一から全てを記述していては日が暮れてしまうため、以下の項目については各自でGoogleしてほしい。

GoogleAppsScriptについて

  • Google Apps Scriptプロジェクトの作成方法
  • claspによるGoogle Apps Scriptのローカル開発
  • Google Cloud Platformのプロジェクト作成方法
  • Google Cloud PlatformでAPIを使用するためのOAuth2.0クライアントIDの取得
  • Google Apps Scriptのプロジェクトを実行可能APIとしてデプロイする

きっと良い記事が見つかることだろう。

以下ではGASでsendEmail()という次の関数を実行可能APIとしてデプロイしたと仮定して進める。

/* これはTypeScriptで書いているため、GASで動かすためには型を取り除く必要がある */
function sendEmail(recipient: string, subject: string, body: string) {
  MailApp.sendEmail(
    recipient,
    subject,
    body
  )
}

Python から GAS関数を実行する

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

Googleが提供しているOAuth2.0およびAPIを呼び出しやすくするためのライブラリをインストールする。

$ pip install google-auth google-auth-oauthlib google-api-python-client

よくチュートリアルにあるoauth2clientdeprecatedとなっている(accessed on 2021/12/4)ため使用しない。Googleもドキュメントを書き換えるべきだが忙しいのだろうか。

Credentialsの取得

よくわからなくてもコピペすれば動くのがチュートリアルコードであるが簡単に解説すると、

  • Credentialsはユーザが同意したGoogleリソースにアクセスするための資格情報である
  • Credentialsはファイルに保存して再利用することが出来る
  • Credentialsが期限切れでリフレッシュトークンがある場合はリフレッシュできる
  • リフレッシュトークンがない場合やそもそもCredentialsが存在しない場合はOAuth2.0フローを通して資格情報を取得する
  • scopeとはアプリケーションが利用するGoogleリソースの範囲・権限である

client_secret.jsonGoogle Cloud PlatformAPIとサービス→認証情報→OAuth2.0クライアントというところから取得する(ググって)

import os
from typing import Optional

from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow

CLIENT_SECRET_FILE = 'client_secret.json'
TOKEN_FILE = 'token.json'
SCOPES = [
    'https://www.googleapis.com/auth/script.scriptapp'
    # 今回はメールを送信する関数しか呼び出さないのでこのスコープだけ
    'https://www.googleapis.com/auth/script.send_mail' 
]

def get_credentials() -> Credentials:
    credentials: Optional[Credentials] = None
    if os.path.exists(TOKEN_FILE):
        credentials = Credentials.from_authorized_user_file(TOKEN_FILE, scopes=SCOPES)
    if credentials and credentials.valid:
        return credentials

    if credentials and credentials.refresh_token:
        credentials.refresh(Request())
        # リフレッシュトークンが期限切れのこともあるのでエラーハンドリングすべき
        return credentials
        
    flow = InstalledAppFlow.from_client_secrets_file(
        CLIENT_SECRET_FILE,
        scopes=SCOPES
    )
    credentials = flow.run_console()
    return credentials


def save_credentials(credentials: Credentials) -> None:
    with open(TOKEN_FILE, 'w') as f:
        f.write(credentials.to_json())

Credentialsを取得するフローはAPIを叩くなら必ず必要なので、ライブラリにするかスニペットに登録するか記憶に叩き込む。

GAS関数を呼び出す

準備が整ったのでGASの関数を呼び出す関数を実装する。

from typing import Any
from googleapiclient import discovery

GAS_DEPLOY_ID = 'hogehoge'  # Google Apps Script の実行可能APIのデプロイID

def call_function(
    func_name: str, 
    parameters: Optional[list[str]] = None
) -> Any:
    body: dict[str, Any] = {
        'function': func_name,
    }
    if parameters:
        body['parameters'] = parameters

    script_service = discovery.build(
        'script',
        'v1',
        credentials=get_credentials()
    )
    response: Any = script_service.scripts().run(
        scriptId=GAS_DEPLOY_ID,
        body=body
    ).execute()
    
    # JavaScriptのオブジェクトをJSON化してPythonのdictにしたものが返ってくる
    return response.get('response').get('result') 

googleapiclientライブラリの使い方は3つくらいのAPIのドキュメントを読めば共通点がつかめる。

  • googleapiclient.discovery.build()で使用するAPIインスタンスを作成
  • インスタンスにメソッドチェーンでAPIのパスを構築
  • 引数はドキュメントを読みながら実装
  • 最後にexecute()メソッドを呼び出してレスポンスを受け取る
  • レスポンスの中身もドキュメントを見ないとわからない

developers.google.com (accessed on 2021/12/4)

最後にcall_function()を使用してGASのsendEmail()を呼び出してみる。

recipient = 'foo@example.com'
subject = 'Test Calling GAS function'
body = 'Success!'
parameters = [recipient, subject, body]
call_function(func_name='sendEmail', parameters=parameters)

無事メールが送信されれば成功。

応用案

今回はメールを送信するだけだったので直接GmailAPIを叩いてもよかった。 しかし、一度GASを通すという今回の実装はGoogleの複数のAPIを利用するときに真価を発揮する。

例えば次のような一連の処理

Python上で実装すると3回HTTP通信を行わなければならない。GASで実装すれば一つのAPI呼び出しだけで終わる。

「全部GAS上でやれば?」というのはもっともな指摘であるが、今回は処理にSeleniumをかませる必要があったのでGAS環境外のリソースを使う必要があったのだ。

GASについて

Google Apps ScriptはGoogleのリソースを使ってスクリプトを実行できるサービスである。 ベースにはJavaScriptが採用されていて、claspというGoogle製のコマンドラインツールを使用すればローカル環境でTypeScriptを使って開発することが出来る。 1日に使用できるAPI呼び出しや外部アクセスに対して制限(accessed on 2021/12/4)が存在するが、個人で使用する分や小規模サービスであればそれほど問題にはならない。

github.com (accessed on 2021/12/4)

Google Apps Scriptを使用する理由として以下のが挙げられる。

  1. 他のGoogleサービスとの連携が容易
  2. Googleのリソースを使用することができる
  3. ベースがJavaScriptなので気軽に始められる

GoogleAPI提供に積極的であるため、どんな言語(Node.js, Python, Go, C#, 単純にHTTPリクエスト)でも容易にAPIを叩くことが出来る。GmailGoogle SpreadSheetsGoogle Driveを連携したシステムを構築したいとなった際、これらの言語で各APIを叩くこともできるが、GAS上で各サービスのAPIを呼び出してGoogleの通信リソースを借用することで自身の通信リソースを節約することが出来る。

おわりに

PythonからGoogle APIの呼び出しをGASを使って一本化するというのは、本当はclaspを使ってTypeScriptでがっつりGASのローカル開発環境を整えてからでないと意味がない。 しかしながら、自分はまだTypeScript初心者であるし(jestもやっとのことで導入できた。。。)、先人がいくつか記事を残してくれていたので今回は詳しく触れないことにする(記憶用の簡単なメモは書くかもしれない)。この記事を読んで「claspなんてあるのか」とか「google-authなんてライブラリがあったのか」と思ってGoogleするきっかけになれば私も嬉しく思う。

このようにGoogleAPIと自社のリソースをエンドユーザにさえ積極的に公開して、世の中のAutomationや業務効率化が進むように貢献している。 それに比べて日本の企業は「我が社にすべてお任せください!」という姿勢ばかりで、自社のリソースをエンドユーザに公開することなど一切ない。

先日MLサービスを比較しようとして、どことは言わないが日本の企業のサイトも見に行ってみた。 驚いたことにサービスの紹介ばかりでチュートリアルなどは一切なく、最後に「資料請求はこちらから」と書いてあったのは爆笑してしまった。 完全に組織のネームバリューに依存したマーケティング戦略で、ユーザにサービスをお試しで使ってもらって競合サービスとの差を実感してもらおうという姿勢が全く見られない。

10か月前ほどにGooglemodel_searchという機械学習のハイパーパラメータを遺伝的(mutation)アルゴリズムによって最適化するライブラリを公開した。

github.com (accessed on 2021/12/4)

Apache2.0ライセンスのもと使用できるオープンソースプロジェクトである。コントリビュータはたったの2人! ただでさえ重いニューラルネットワーク学習タスクに遺伝的アルゴリズムを加えて計算機パワーでAIエンジニアの仕事を奪う楽にするという素晴らしいライブラリだ。 もちろんこんなものエンドユーザのローカル環境で動かせるはずもなく、中小企業でも難しいと思われる。 当然Googleはビジネスが上手いので、自社のクラウドコンピューティング環境を実行環境として提供している。

  1. ライブラリを公開して社会が潤う
  2. ライブラリの使用にクラウドが必要なのでGoogleも潤う
  3. 潤った資源を研究者に配分して新技術をライブラリとして公開

という社会も企業も研究者も得するサイクルがそこには存在する。 日本の企業で新技術をライブラリとして公開しているとかLinuxディストリビューションを頒布しているとかあったら教えてください。就活の参考にします。 脆弱性対応の分野では結構進んでいる方だと思うけれど、脆弱性潰しても儲からん。。。

確か先月あたりにMicrosoftもAutoMLのライブラリを公開していた。名前は忘れた。