仕事はOutlook、外出先やスマホはGoogleカレンダー——そんな二刀流をやっていると、毎回手動で予定を転記する地味な作業が発生しませんか?
「あ、Outlookに入れた会議、Googleに反映するの忘れてた」という経験を繰り返した末に、PythonでOutlookの予定をGoogleカレンダーへ自動同期するツールを作りました。
この記事では、そのツール(sync_calendar.py)の仕組みと使い方を紹介します。
更新履歴
| 日付 | 内容 |
|---|---|
| 2026/05/19 | V1.1公開(※V1.0は欠番) |
| 2026/05/22 | V1.2公開 ※繰り返し予定の一部だけ日時変更した場合にGoogleカレンダーへ反映されないバグを修正 |
注意事項
sync_calendar.pyをV1.1→V1.2に変更時には実行前に、以下を実施して下さい。
- Googleカレンダー側で、descriptionに「Outlook連携」と書かれている予定を全部削除
- synced_events.json を削除
このツールでできること
- Outlookの予定を取得してGoogleカレンダーへコピーする
- 同期範囲は実行日〜約1か月先(過去2日分も対象)
- 重複防止:何度実行しても同じ予定が重複してコピーされない
- 差分更新:Outlookで予定を変更すると、Googleカレンダー側も自動更新
- 削除同期:Outlookで消した予定は、Googleカレンダーからも削除
- Googleカレンダーで独自に追加した予定には一切干渉しない
既製ツール(OGCS、OneCal、Zapierなど)は多機能な反面、設定が複雑になりがちです。このツールはシンプルで軽量なので、用途に合わせてカスタムしやすいのが特徴です。
動作環境
- OS:Windows 10 / 11
- Outlook:Windows版デスクトップアプリ
- Python:3.x
準備①:ライブラリのインストール
コマンドプロンプトで以下を実行してください。
pip install pywin32 google-api-python-client google-auth-httplib2 google-auth-oauthlib
準備②:Google Cloud Consoleの設定
Google Calendar APIを使うには、Google Cloud側でいくつかの設定が必要です。手順通りに進めれば難しくありません。
1. プロジェクトの作成とAPIの有効化
- Google Cloud Console にアクセスし、新しいプロジェクトを作成
- 「APIとサービス」→「ライブラリ」から「Google Calendar API」を検索して「有効にする」
2. OAuth同意画面の設定
- 「APIとサービス」→「OAuth同意画面」を選択
- 「User Type」で「外部」を選択して「作成」
- アプリ名(例:
sync_calendar)、サポートメール、連絡先を入力して「保存して次へ」 - 「スコープを追加または削除」から
.../auth/calendarを追加
3. テストユーザーの追加
- 「テストユーザー」→「+ ADD USERS」をクリック
- 自分のGoogleメールアドレスを入力して「追加」
4. 認証情報(credentials.json)の取得
- 「APIとサービス」→「認証情報」→「+ 認証情報を作成」→「OAuthクライアントID」
- アプリケーションの種類:「デスクトップアプリ」を選択して作成
- 「JSONをダウンロード」でファイルを保存し、
credentials.jsonにリネーム
フォルダ構成
スクリプトと各ファイルは同じフォルダに置いてください。
C:\sync\
├ sync_calendar.py
├ credentials.json ← Google Cloudから取得(手動で配置)
├ token.json ← 初回実行時に自動生成
└ synced_events.json ← 実行のたびに自動更新
| ファイル | 役割 |
|---|---|
credentials.json |
Googleへのアクセス権を証明するファイル。Google Cloudから取得 |
token.json |
ログイン状態を保存するファイル。初回認証後に自動生成される |
synced_events.json |
OutlookとGoogleの予定の対応関係を記録するファイル |
token.json は他人に渡すと、そのままGoogleカレンダーを操作できてしまいます。取り扱いに注意してください。
ソースコード
import win32com.client
from datetime import datetime, timedelta
import json
import os
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
# ======================
# 設定
# ======================
SCOPES = ['https://www.googleapis.com/auth/calendar']
STATE_FILE = "synced_events.json"
# ======================
# Outlook予定取得
# ======================
def get_outlook_events():
outlook = win32com.client.Dispatch("Outlook.Application")
namespace = outlook.GetNamespace("MAPI")
calendar = namespace.GetDefaultFolder(9)
items = calendar.Items
items.IncludeRecurrences = True
items.Sort("[Start]")
# V1.1 old:start = datetime_now()
start = datetime.now() - timedelta(days=2)
end = start + timedelta(days=30)
# V1.2
start_str = start.strftime("%m/%d/%Y")
end_str = end.strftime("%m/%d/%Y")
restriction = f"[Start] >= '{start_str}' AND [Start] <= '{end_str}'"
restricted_items = items.Restrict(restriction)
events = {}
for item in restricted_items:
try:
# v1.2
start_val = item.Start
end_val = item.End
if not start_val or not end_val:
continue
key = item.EntryID + start_val.strftime("%Y-%m-%dT%H:%M:%S")
events[key] = {
"title": item.Subject or "",
"start": start_val.strftime("%Y-%m-%dT%H:%M:%S"),
"end": end_val.strftime("%Y-%m-%dT%H:%M:%S"),
"location": item.Location or "",
"updated": str(item.LastModificationTime)
}
except Exception as e:
# v1.2
print("エラー:", e)
continue
return events
# ======================
# Google認証
# ======================
def get_google_service():
creds = None
if os.path.exists("token.json"):
creds = Credentials.from_authorized_user_file("token.json", SCOPES)
# v1.1
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request()) # V1.1(自動更新)
elif not creds or not creds.valid:
flow = InstalledAppFlow.from_client_secrets_file("credentials.json", SCOPES)
creds = flow.run_local_server(port=0)
with open("token.json", "w") as f:
f.write(creds.to_json())
return build("calendar", "v3", credentials=creds)
# ======================
# Google操作
# ======================
def add_event(service, oid, e):
body = {
"summary": e["title"],
"location": e["location"],
"start": {
"dateTime": e["start"],
"timeZone": "Asia/Tokyo"
},
"end": {
"dateTime": e["end"],
"timeZone": "Asia/Tokyo"
},
"description": f"Outlook連携\nOID:{oid}",
# "通知"を無しに設定
"reminders": {
"useDefault": False
}
}
created = service.events().insert(calendarId='primary', body=body).execute()
return created["id"]
def update_event(service, gid, oid, e):
body = {
"summary": e["title"],
"location": e["location"],
"start": {
"dateTime": e["start"],
"timeZone": "Asia/Tokyo"
},
"end": {
"dateTime": e["end"],
"timeZone": "Asia/Tokyo"
},
"description": f"Outlook連携\nOID:{oid}",
# v1.2
"reminders": {
"useDefault": False
}
}
service.events().update(calendarId='primary', eventId=gid, body=body).execute()
def delete_event(service, gid):
service.events().delete(calendarId='primary', eventId=gid).execute()
# ======================
# 状態管理
# ======================
def load_state():
if os.path.exists(STATE_FILE):
with open(STATE_FILE, "r") as f:
return json.load(f)
return {}
def save_state(state):
with open(STATE_FILE, "w") as f:
json.dump(state, f, indent=2)
# ======================
# メイン
# ======================
def main():
print("==== 同期開始 ====")
prev_state = load_state()
outlook_events = get_outlook_events()
service = get_google_service()
new_state = {}
add_count = 0
update_count = 0
delete_count = 0
# ===== ① 追加・更新 =====
for oid, e in outlook_events.items():
if oid not in prev_state:
gid = add_event(service, oid, e)
add_count += 1
else:
gid = prev_state[oid]["google_id"]
if prev_state[oid]["updated"] != e["updated"]:
update_event(service, gid, oid, e)
update_count += 1
new_state[oid] = {
"google_id": gid,
"updated": e["updated"]
}
# ===== ② 削除検知 =====
for oid in prev_state:
if oid not in outlook_events:
gid = prev_state[oid]["google_id"]
try:
delete_event(service, gid)
delete_count += 1
except:
pass
save_state(new_state)
print(f"追加: {add_count}件")
print(f"更新: {update_count}件")
print(f"削除: {delete_count}件")
print("==== 完了 ====")
if __name__ == "__main__":
main()
実行方法
事前にOutlookを起動した状態で、コンソールから実行します。
python sync_calendar.py
初回実行時はブラウザが開き、Googleアカウントへのログインと権限許可が求められます。許可するとtoken.jsonが生成され、2回目以降は自動で実行されます。
実行結果の例:
==== 同期開始 ====
追加: 3件
更新: 1件
削除: 0件
==== 完了 ====
技術的な仕組み
Outlookへのアクセス(win32com)
Outlookの操作にはwin32comライブラリを使用しています。これはWindowsのCOM(Component Object Model) という仕組みを通じて、PythonからOfficeアプリを操作するためのライブラリです。
本ツール → win32com → COM → Outlookアプリ
Googleカレンダーへのアクセス(Google API Client Library)
GoogleカレンダーはREST API経由でアクセスします。google-api-python-clientがその橋渡しをします。
本ツール → Google API Client Library → Google Calendar REST API
使用しているAPIは以下の3つです。
| 操作 | API |
|---|---|
| 予定追加 | events.insert |
| 予定更新 | events.update |
| 予定削除 | events.delete |
差分管理(synced_events.json)
重複防止・更新検知・削除検知はすべてsynced_events.jsonで管理しています。OutlookのEntryIDとGoogleのイベントIDの対応関係を記録しておくことで、毎回の実行で差分だけを処理します。
{
"00000000C1A8C1…504F0000": {
"google_id": "92dutlq9ot91jodtccackikvbs",
"updated": "2026-05-11 13:29:34.788000+00:00"
}
}
v1.2での繰り返し予定バグの修正について
v1.1までは、Outlookの予定を識別するキーとしてEntryIDのみを使用していました。しかし繰り返し設定の予定では、各回のEntryIDが同一になるため、特定回の日時だけ変更した場合に別の予定として認識できないバグがありました。
v1.2では、キーを EntryID + 開始日時 の組み合わせに変更することで、繰り返し予定の各回を個別に識別できるようにしました。
# v1.1まで(繰り返し予定の各回を区別できない)
key = item.EntryID
# v1.2以降(開始日時を組み合わせて各回を区別)
key = item.EntryID + start_val.strftime("%Y-%m-%dT%H:%M:%S")
制限事項
- 単一PC専用:同じOutlook・Googleカレンダーに対して複数のPCから同時実行すると、状態管理ファイルが競合して予期しない動作になります。
おわりに
シンプルに作ることを意識したので、コード全体で150行程度に収まっています。カスタムポイントとしては、同期範囲の日数(現在は30日)や、同期対象のカレンダーフォルダの変更などが比較的簡単にできます。
同じ悩みを持っている方の参考になれば幸いです。
その他の記事は BB研究所 で公開しています。