PythonでGoogleカレンダーの予定を自動取得・整理する方法

Googleカレンダー自動化の背景

複数のプロジェクトを掛け持ちしていると、「今日の予定」「今週の空き時間」を確認するだけで時間がかかります。Google Calendar APIとPythonを組み合わせれば、予定の一覧取得、空き時間の算出、Slackへの自動通知まで自動化できます。

この記事では、Google Calendar APIのセットアップからPythonでの予定取得・整理までの手順を解説します。

Google Cloud Console の設定

1. プロジェクトの作成

Google Cloud Consoleでプロジェクトを作成します。

2. Google Calendar APIの有効化

「APIとサービス」→「ライブラリ」で「Google Calendar API」を検索して有効化します。

3. 認証情報の作成

「APIとサービス」→「認証情報」→「認証情報を作成」→「OAuthクライアントID」を選択。アプリケーションの種類は「デスクトップアプリ」を選びます。作成後、credentials.jsonをダウンロードします。

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

pip install google-auth-oauthlib google-auth-httplib2 google-api-python-client python-dotenv

OAuth認証フローの実装

import os
from pathlib import Path
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.discovery import build

SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"]
TOKEN_PATH = Path("token.json")
CREDENTIALS_PATH = Path("credentials.json")


def get_calendar_service():
    """認証済みのGoogle Calendar APIサービスを返す"""
    creds = None

    # 既存のトークンを読み込む
    if TOKEN_PATH.exists():
        creds = Credentials.from_authorized_user_file(str(TOKEN_PATH), SCOPES)

    # トークンが無効または期限切れの場合は更新
    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(
                str(CREDENTIALS_PATH), SCOPES
            )
            creds = flow.run_local_server(port=0)

        # トークンを保存
        TOKEN_PATH.write_text(creds.to_json())

    return build("calendar", "v3", credentials=creds)

初回実行時はブラウザが開いてGoogleアカウントでの認証を求められます。認証後はtoken.jsonが生成され、2回目以降は自動ログインします。

今日の予定を取得する

from datetime import datetime, timezone, timedelta


def get_today_events(service) -> list[dict]:
    """今日の予定を取得する"""
    jst = timezone(timedelta(hours=9))
    now = datetime.now(jst)
    today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
    today_end = now.replace(hour=23, minute=59, second=59, microsecond=0)

    events_result = service.events().list(
        calendarId="primary",
        timeMin=today_start.isoformat(),
        timeMax=today_end.isoformat(),
        singleEvents=True,
        orderBy="startTime",
    ).execute()

    events = events_result.get("items", [])
    result = []

    for event in events:
        start = event["start"].get("dateTime", event["start"].get("date"))
        end = event["end"].get("dateTime", event["end"].get("date"))
        result.append({
            "title": event.get("summary", "(タイトルなし)"),
            "start": start,
            "end": end,
            "location": event.get("location", ""),
        })

    return result


# 使用例
service = get_calendar_service()
events = get_today_events(service)
for e in events:
    print(f"{e['start']} - {e['title']}")

今週の空き時間を計算する

def get_free_slots(service, work_start_hour: int = 9, work_end_hour: int = 18) -> list[dict]:
    """今週の空き時間スロットを返す"""
    jst = timezone(timedelta(hours=9))
    now = datetime.now(jst)
    week_start = now - timedelta(days=now.weekday())
    week_start = week_start.replace(hour=0, minute=0, second=0, microsecond=0)
    week_end = week_start + timedelta(days=7)

    events_result = service.events().list(
        calendarId="primary",
        timeMin=week_start.isoformat(),
        timeMax=week_end.isoformat(),
        singleEvents=True,
        orderBy="startTime",
    ).execute()

    busy_times = []
    for event in events_result.get("items", []):
        if "dateTime" in event["start"]:
            start = datetime.fromisoformat(event["start"]["dateTime"])
            end = datetime.fromisoformat(event["end"]["dateTime"])
            busy_times.append((start, end))

    free_slots = []
    for day_offset in range(5):  # 月〜金
        day = week_start + timedelta(days=day_offset)
        work_start = day.replace(hour=work_start_hour)
        work_end = day.replace(hour=work_end_hour)

        day_busy = sorted([
            (s, e) for s, e in busy_times
            if s.date() == day.date()
        ])

        current = work_start
        for busy_start, busy_end in day_busy:
            if current < busy_start:
                free_slots.append({
                    "date": day.strftime("%Y-%m-%d (%a)"),
                    "start": current.strftime("%H:%M"),
                    "end": busy_start.strftime("%H:%M"),
                })
            current = max(current, busy_end)

        if current < work_end:
            free_slots.append({
                "date": day.strftime("%Y-%m-%d (%a)"),
                "start": current.strftime("%H:%M"),
                "end": work_end.strftime("%H:%M"),
            })

    return free_slots

実行して日程調整に活用する

if __name__ == "__main__":
    service = get_calendar_service()

    print("=== 今日の予定 ===")
    for e in get_today_events(service):
        print(f"  {e['start'][:16]} {e['title']}")

    print("\n=== 今週の空き時間 ===")
    for slot in get_free_slots(service):
        print(f"  {slot['date']} {slot['start']}〜{slot['end']}")

このスクリプトをSlack通知と組み合わせれば、毎朝「今日の予定と空き時間」を自動でSlackに投稿する仕組みが作れます。

まとめ

Google Calendar APIとPythonを組み合わせることで、以下が実現できます。

  • OAuthトークンを`token.json`にキャッシュすることで2回目以降の認証が不要になる
  • `events().list()`で期間指定の予定一覧を取得できる
  • 取得したイベントを加工して空き時間を算出できる
  • 予定管理の自動化は、複数プロジェクトをこなすエンジニアにとって地味に効果が大きいです。Slack通知と組み合わせた活用例は別記事でも紹介しているので、参考にしてみてください。