Googleカレンダーの任意の予定をPythonで抽出する方法


#はじめに

今回は以下のようなカレンダーから任意の予定を抽出していきたいと思います。**任意の予定**を表示させる方法というのがわからなくて、結構しらべてしまいました。結局は公式ドキュメントをみれば一発だったのですが…

そこで「どうせなら全部書くかー」と思い、Qiitaに書き込んだまでです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556110/31197862-8995-a189-63d3-68a4d4695a74.png)




#導入
基本の設定は公式ドキュメントの[Python Quickstar](https://developers.google.com/calendar/quickstart/python)に書いてあるので詳細は飛ばします。

必要なもの

- credentials.jsonファイル
- カレンダーID
- Google Clientのライブラリ

ファイルは[こちら](https://developers.google.com/calendar/quickstart/python)からダウンロードできます。これから作る`main.py`のスクリプトと同じディレクトリに入れてください。

カレンダーIDは自分のGoogleカレンダーの設定に載っています。わからない場合はググるか、[こちら](https://support.google.com/a/answer/1626902?hl=ja)を参照してみてください。もし[マイカレンダー]→[名前]を使うのであれば、カレンダーIDはGmailアドレスとなります。それ以外（例えば以下の図の「Test」など）は調べないとわかりません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556110/355cec4e-31bb-ae64-3401-694ae6cd36cc.png)

Google Clientのライブラリは、インストールしていなければインストールしてください。

```
pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib
```

#公式サンプル

公式ドキュメントのサンプルは以下のようになっています。これを少し改良すれば、好き放題できそうです。

```sample1.py
from __future__ import print_function
import datetime
import pickle
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request

# If modifying these scopes, delete the file token.pickle.
SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']

def main():
    """Shows basic usage of the Google Calendar API.
    Prints the start and name of the next 10 events on the user's calendar.
    """
    creds = None
    # The file token.pickle stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first
    # time.
    if os.path.exists('token.pickle'):
        with open('token.pickle', 'rb') as token:
            creds = pickle.load(token)
    # If there are no (valid) credentials available, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json', SCOPES)
            creds = flow.run_local_server(port=0)
        # Save the credentials for the next run
        with open('token.pickle', 'wb') as token:
            pickle.dump(creds, token)

    service = build('calendar', 'v3', credentials=creds)

    # Call the Calendar API
    now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time
    print('Getting the upcoming 10 events')
    events_result = service.events().list(calendarId='primary', timeMin=now,
                                        maxResults=10, singleEvents=True,
                                        orderBy='startTime').execute()
    events = events_result.get('items', [])

    if not events:
        print('No upcoming events found.')
    for event in events:
        start = event['start'].get('dateTime', event['start'].get('date'))
        print(start, event['summary'])

if __name__ == '__main__':
    main()
```

#任意の予定検索
公式ドキュメントの前半はごちゃごちゃとコードが書いてありますが、基本はそのままで大丈夫です。` # Call the Calendar API`のあたりからコードを変形させていきます。まず`events_result`のリストは、サンプルとして以下のように設定しておきます。

```.py
events_result = service.events().list(
        calendarId = calendar_id,
        q = "test",
        timeMin = "2020-02-01T00:00:00+00:00",
        timeMax = "2020-02-29T00:00:00+00:00",
        timeZone = None,
        singleEvents = True,
        orderBy = "startTime",
        ).execute()
events = events_result.get("items", [])
```

`q = "test"`は検索するキーワードを指しています。リストの要素は[こちら](https://developers.google.com/calendar/v3/reference/events/list)に色々と書いてあるので必要になったらみてください。

サンプルとしては2020年2月の予定を表示したいので`timeMin`と`timeMax`は2月の範囲を`datetime`で指定します。ここは注意が必要です。

# 表示を整える

次に、その後のスクリプトを改良していきます。ここはご自由に設定して良いのですが、Googleカレンダーの説明`description`を読み込むときには注意が必要です。

```.py
if not events:
    print("not found")
for event in events:
    start = event['start'].get('dateTime',event['start'].get('date'))
    description = event.get('description', "not found")
    print(start)
    print("Event :",event['summary'])
    print("Description :",description,"\n")
```

`event['description']`ではなく、`description = event.get('description', "not found")`として`get.()`を使用します。ちなみに予定に説明文が書かれていなかったときには`not found`を返します。

`start = event['start'].get('dateTime',event['start'].get('date'))`でも同じように、予定に時間設定がある場合は、`'dateTime'`を返し、予定が終日の場合は`event['start'].get('date')`を返すようにしています。

# 実装
以上のコードを繋げていきます。一応完成したコートをのせていますが、こちらは他の作業のために自分用に作っているので、下手にクラスや関数を使っていることは大目に見てください。

```main.py
from __future__ import print_function
import pickle
import os.path

from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request

# If modifying these scopes, delete the file token.pickle.
SCOPES = ['https://www.googleapis.com/auth/calendar']

calendar_id = "your calendar id"

class Writer(object):
    def __init__(self):
        """Shows basic usage of the Google Calendar API.
            Prints the start and name of the next 10 events on the user's calendar.
            """
        creds = None
        # The file token.pickle stores the user's access and refresh tokens, and is
        # created automatically when the authorization flow completes for the first
        # time.
        if os.path.exists('token.pickle'):
            with open('token.pickle', 'rb') as token:
                creds = pickle.load(token)
        # If there are no (valid) credentials available, let the user log in.
        if not creds or not creds.valid:
            if creds and creds.expired and creds.refresh_token:
                creds.refresh(Request())
            else:
                flow = InstalledAppFlow.from_client_secrets_file(
                    'credentials.json', SCOPES)
                creds = flow.run_local_server(port=0)
            # Save the credentials for the next run
            with open('token.pickle', 'wb') as token:
                pickle.dump(creds, token)

        self.service = build('calendar', 'v3', credentials=creds)

    def google_calendar_reader(self):

            events_result = self.service.events().list(
                calendarId= calendar_id,
                q="test",
                timeMin = "2020-02-01T00:00:00+00:00",
                timeMax = "2020-02-29T00:00:00+00:00",
                timeZone = None,
                singleEvents=True,
                orderBy="startTime",
                ).execute()
            events = events_result.get("items", [])

            if not events:
                print("not found")
            for event in events:
                start = event['start'].get('dateTime',event['start'].get('date'))
                description = event.get('description', "not found")
                print(start)
                print("Event :",event['summary'])
                print("Description :",description,"\n")

writer = Writer()
writer.google_calendar_reader()
```
出力結果はこのようになりました。

```出力結果
2020-02-03
Event : test 1
Description : This is description of test_1. 

2020-02-10T10:00:00+09:00
Event : test 3
Description : not found 

2020-02-11
Event : Test
Description : not found 
```

さて、しっかりと抽出できたといわれれば、そうではないことがわかります。もう一度、サンプルのカレンダーを見てみます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556110/31197862-8995-a189-63d3-68a4d4695a74.png)


Testカレンダーにある「テスト」はもちろん表示されませんが、「test_4」も表示されていませんね。逆に大文字の「Test」は出力されています。ここで使う`q=`はどうやら綴りが完全一致の単語を検索しているのだということが読み取れます。

したがって「テスト」を表示したい場合は`q="テスト"`、「test_4」を表示したい場合は`q=test_4`とする必要があります。

#まとめ

用途によっては色々と使えそうですね。自分の予定からどれくらいの頻度で、何をしているのかといったような行動分析もできるような気がします。

私はまた違った使い方をしようと思っているのですが…

何かのお役に立てれば幸いです。間違っている箇所があれば指摘お願いします。（`""`と`''`がごちゃごちゃになっているのはご了承ください。すいません）

# 参考文献
- [event.list()](https://developers.google.com/calendar/v3/reference/events/list)の公式ドキュメント
- [GoogleカレンダーAPI Python クイックスタート](https://developers.google.com/calendar/quickstart/python)
- [Googleカレンダーで各カレンダーのカレンダーIDを知る方法](http://www.sukicomi.net/2018/07/google-calendarid.html)