この仕組みは、私とAIアシスタントとの雑談に近いやり取りから生まれました。
毎日のSNSの投稿予約スケジュールをObsidian上のマークダウンで管理していた際、ふと「当日の行動管理をしやすくするために、このSNS予定をGoogleカレンダーにも入れておいた方がいいだろうか?」と呟いたのがきっかけです。
しかし、手動でコピペしてカレンダーに登録するのはあまりに面倒ですし、かといってGoogleカレンダーのAPIを紐付けるためのOAuth認証やAPIキーの設定は個人でやるには大掛かりすぎて辟易します。
その時、AIアシスタントから提案されたのが、「カレンダーの標準規格である.icsファイル(iCalendar)をローカルのPythonで自動生成し、Googleカレンダーに『URLで購読』させる方式」でした。
これなら、
- Google Cloud等の面倒なAPI登録や認証キーの管理は一切不要(Zero-Auth)
- 面倒なカレンダー入力作業から完全に解放される
- 無料のツール(PythonとObsidian)だけで完結する
という、最もシンプルでエコな自動化が実現できます。
この記事では、この「予定入力の摩擦(フリクション)」をゼロにするシステムの作り方を、実際に私たちが検証して動作したコードと共に解説します。
💡 解決策:iCalendar(.ics)ファイル配信方式
通常、Googleカレンダーと外部プログラムを連携させるには、Google Cloud Consoleでプロジェクトを作成し、認証用の秘密鍵(JSON)を取得し、複雑なOAuth設定を行う必要があります。これは個人で使用するにはあまりにもオーバースペックです。
そこで今回は、カレンダーの標準規格である iCalendar(.ics)ファイル をPythonで自動生成し、それをGoogleカレンダーに「購読(URLで追加)」させる方式を採用します。
全体の流れ
- 予定の入力 (Obsidian): いつも通りMarkdownのテーブル(表)に予定を入力します。
- ファイル生成 (Python): PythonスクリプトがObsidianのファイルを監視・解析し、標準的なカレンダーデータファイル(
schedule.ics)を書き出します。 - カレンダー同期 (Google): 生成したファイルをローカルの簡易ウェブサーバー(またはDropbox、GitHub Pages等)に配置し、そのURLをGoogleカレンダーに登録します。Googleが定期的にこのURLをフェッチし、予定が自動更新されます。
⚠️ 実装前に知っておくべき制約事項(セキュリティと同期ラグ)
この「API不要の配信方式」はシンプルですが、技術的な制約が2点あります。設計前に必ず理解しておいてください。
- セキュリティ(カレンダーURLの秘匿)
Googleカレンダーに「URLで購読」させるため、生成した.icsファイルはインターネットからアクセス可能な場所に配置する必要があります。このURLが第三者に漏洩した場合、カレンダーの予定タイトルやメモが閲覧可能になります。
- 対策: ファイル名やURLにランダムな文字列(Dropboxの共有リンクや推測困難なファイル名)を含め、他人に推測されない工夫を行ってください。また、ファイル内には個人情報やパスワードなどの機密データを含めないようにしてください。
- 同期のタイムラグ(Google側の制限)
Googleカレンダーの外部URL購読は、リアルタイム同期ではありません。Googleのサーバーが定期的にフェッチ(取得)する仕様のため、Obsidianを編集してからカレンダーに反映されるまで 数時間〜最大24時間の時間差(同期ラグ) が生じます。
- 対策: 即時同期が必要な「1時間後のミーティング予定」などには向いていません。数日〜数週間先の「SNS発信予定」「イベントスケジュール」といった、中長期的な予定の俯瞰用として活用するのがベストです。
🛠️ 実装手順
1. Obsidianに予定表テーブルを作成する
以下のように、ObsidianのノートにMarkdown形式の予定管理テーブルを用意します。今回は例として SNS投稿予定スケジュール.md とします。
# 📅 投稿予約スケジュール表
| 予定日時 | ステータス | メモ |
| :--- | :--- | :--- |
| [[2026-06-15 18:00]] | ✅ 予約済 | NotionとObsidianの役割分担について発信 |
| [[2026-06-16 08:00]] | ⏳ 未セット | コーヒーTDS測定の基本と再現性の科学 |
2. カレンダー生成用のPythonスクリプトを作成する
外部ライブラリを一切使わず、Pythonの標準ライブラリ(re、datetime)だけで動くシンプルなスクリプトです。
sync_calendar.py という名前で保存してください。
import os
import re
from datetime import datetime, timedelta
# --- 設定項目 ---
# Obsidianのスケジュールファイルへの絶対パス
OBSIDIAN_FILE_PATH = "/Users/YOUR_USERNAME/Documents/Vault/SNS投稿予定スケジュール.md"
# 出力するカレンダーファイル(.ics)の保存先
OUTPUT_ICS_PATH = "./schedule.ics"
def parse_obsidian_schedule(file_path):
events = []
if not os.path.exists(file_path):
print(f"File not found: {file_path}")
return events
with open(file_path, "r", encoding="utf-8") as f:
for line in f:
# テーブルの行(| で始まる行)のみ処理対象とする
if not line.strip().startswith("|"):
continue
# ヘッダー行や区切り行はスキップする
if "予定日時" in line or "---" in line or "ステータス" in line:
continue
# 行を | で分割し、前後の空白を除去してリスト化
columns = [col.strip() for col in line.split("|")[1:-1]]
if len(columns) < 2:
continue
# 1列目の [[リンク]] から日付テキストを抽出
date_col = columns[0].replace("[[", "").replace("]]", "")
# [[ファイル名|表示テキスト]] のようなエイリアス表記の場合、右側の表示テキストを採用
if "|" in date_col:
time_str = date_col.split("|")[1].strip()
else:
time_str = date_col.strip()
# 2列目はステータス、最後の列をメモとする(列数が増えても動作を維持)
status = columns[1]
memo = columns[-1]
# 複数の日付フォーマットに対応してパースを試みる
start_dt = None
for fmt in ("%Y-%m-%d %H:%M", "%Y/%m/%d %H:%M", "%Y-%m-%d %H:%M:%S"):
try:
start_dt = datetime.strptime(time_str, fmt)
break
except ValueError:
continue
# パースに失敗した行はスキップ
if not start_dt:
continue
end_dt = start_dt + timedelta(hours=1)
events.append({
"summary": f"[{status}] {memo[:20]}...",
"description": f"ステータス: {status}\nメモ: {memo}",
"start": start_dt,
"end": end_dt
})
return events
def generate_ics(events, output_path):
lines = [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//Obsidian Sync Agent//JP",
"CALSCALE:GREGORIAN",
"METHOD:PUBLISH",
"X-WR-CALNAME:Obsidian予定表",
"X-WR-TIMEZONE:Asia/Tokyo"
]
for idx, ev in enumerate(events):
dtstart = ev["start"].strftime("%Y%m%dT%H%M%S")
dtend = ev["end"].strftime("%Y%m%dT%H%M%S")
# 簡易的な一意のUIDを生成
uid = f"obsidian-event-{idx}-{dtstart}@036factory.local"
lines.extend([
"BEGIN:VEVENT",
f"UID:{uid}",
f"DTSTART;TZID=Asia/Tokyo:{dtstart}",
f"DTEND;TZID=Asia/Tokyo:{dtend}",
f"SUMMARY:{ev['summary']}",
f"DESCRIPTION:{ev['description']}",
"END:VEVENT"
])
lines.append("END:VCALENDAR")
# ファイル破損を防ぐため、一時ファイルに書き込んでからアトミックに置換する
temp_path = output_path + ".tmp"
try:
with open(temp_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
os.replace(temp_path, output_path)
print(f"ICS file generated successfully (atomically): {output_path}")
except Exception as e:
if os.path.exists(temp_path):
os.remove(temp_path)
raise e
if __name__ == "__main__":
events = parse_obsidian_schedule(OBSIDIAN_FILE_PATH)
generate_ics(events, OUTPUT_ICS_PATH)
🤖 AIエージェントに「丸投げ」して実装する方法
「Pythonコードを自分で作成したり、ファイルパスを書き換えたりするのが面倒」という方、あるいはノンプログラマーの方は、AIにこの実装を丸ごと任せてしまうのが最も手軽です。
CursorやWindsurfなどのAIエージェント、あるいはChatGPTやClaude、GitHub Copilot(Codex)といったチャットAIツールを使っている場合は、この記事をそのままコピーして以下のようなプロンプトと一緒にAIに渡してください。
🤖 AIへのプロンプト例:
「この記事を参考にして、私の環境でObsidianの予定表からGoogleカレンダー用の.icsファイルを生成するPythonスクリプトを構築し、動くようにセットアップしてください。
- 私のObsidianのファイルパスは
[あなたのファイルの絶対パス]です。- 出力ファイルは
[出力したいフォルダの絶対パス]/schedule.icsにしてください。」
AIはコードをあなたの環境に合わせて最適化し、パスを書き換えた完成済みのスクリプトファイルを自動で作成してくれます。自律型エージェントであれば、動作検証テストまで裏で自動で片付けてくれます。
3. Googleカレンダーに購読URLを登録する
- 生成された
schedule.icsを、外部からアクセス可能な場所に配置します(ローカルで常時起動しているサーバーや、Dropboxの共有リンク、GitHub Pages、プライベートなWebサーバーなど)。 - Googleカレンダーを開きます。
- 左側の「他のカレンダー」の横にある「+」アイコンをクリックし、「URLで追加」 を選択します。
- 生成された
.icsファイルのURLを入力し、「カレンダーを追加」をクリックします。
これで、Googleカレンダー上にObsidianの予定表がレイヤーとして重ねて表示されるようになります!
🛠️ 自力で実装するのが難しい、もっと効率化したい方へ
「Pythonのスクリプトを動かす環境が作れない」「エラーが出てうまく動かない」「カレンダーから双方向に同期させたい」など、環境の構築やカスタマイズでお困りの場合は、個別での導入アシスト(有料サポート)を承っております。
あなたの業務フローに合わせて最適な形で導入支援をいたしますので、ご希望の方は公式LINE、またはお問い合わせ窓口よりお気軽にご連絡ください!