Obsidianで予定を管理していると、「この予定をGoogleカレンダーでも確認できたら便利なのに」と思うことがあります。
ブログ記事の公開予定、SNSの投稿予定、勉強や作業のスケジュールなどをObsidianで管理している場合、同じ予定をGoogleカレンダーへ手入力するのは意外と面倒です。
Google Calendar APIを利用すれば、PythonからGoogleカレンダーを操作できます。しかし、APIを利用するにはGoogle Cloud側の設定や認証などが必要になります。
そこで今回は、Google Calendar APIを使わず、Obsidianの予定をPythonでICS(iCalendar)形式に変換し、Googleカレンダーから購読できる形にする方法を紹介します。
仕組みはシンプルです。
Obsidian → Python → ICSファイル → Googleカレンダー
この記事では、Obsidian側の予定表を用意するところから、PythonでICSファイルを生成し、Googleカレンダーへ追加するまでを順番に解説します。
さらに、この方法がどこまで「自動化」できるのか、リアルタイム同期との違い、UIDやタイムゾーンなどiCalendar仕様上の注意点、Google Calendar APIを使うべきケースについても整理します。
この記事でできること
この記事の方法を使うと、Obsidianで管理している次のような予定をGoogleカレンダーでも確認できるようになります。
- ブログ記事の公開予定
- SNS投稿予定
- 勉強・学習予定
- 作業スケジュール
- イベント予定
- コンテンツ制作の予定
ポイントは、予定の管理場所をObsidianからGoogleカレンダーへ移さないことです。Obsidianを予定管理の中心として使いながら、Googleカレンダーを確認用として利用します。
💡 Google Calendar APIを使わずに連携する仕組み
Googleカレンダーとプログラムを連携する代表的な方法が、Google Calendar APIです。APIを利用すれば、プログラムから予定の作成、変更、削除などを細かく操作できます。
一方、今回の目的が「Obsidianで管理している予定をGoogleカレンダーでも確認したい」というものであれば、APIを使わない方法もあります。
そこで利用するのが ICS(iCalendar) です。
ICSは、カレンダー情報を交換するための標準的な形式です。iCalendarはRFC 5545で仕様が定義されています。
今回の仕組みでは、PythonでObsidianのMarkdownファイルを読み取り、その内容をICSファイルとして出力します。そのICSファイルをGoogleカレンダーから取得できるURLに配置し、Googleカレンダー側から追加します。
全体像は次のとおりです。
Obsidian
↓
予定を記述したMarkdown
↓
Pythonで読み取り
↓
ICSファイルを生成
↓
外部から取得できるURLに配置
↓
GoogleカレンダーからURLで追加
この方法なら、Google Calendar APIを直接操作するプログラムを作らなくても、Obsidianの予定をGoogleカレンダーで確認できます。
⚠️ 実装前に知っておきたい前提と制約
1.この方法は「リアルタイム同期」ではない
ここは重要です。今回の方法を「ObsidianとGoogleカレンダーの完全同期」と考えると、実際の動作との違いが生じます。
基本的には、
Obsidian → Python → ICS → Googleカレンダー
という一方向の連携です。
Obsidian側で予定を変更すると、PythonでICSファイルを更新できます。しかし、その直後にGoogleカレンダーへ必ず反映されるわけではありません。Googleカレンダー側が外部カレンダーのデータを取得するタイミングによって、反映まで時間差が生じる可能性があります。
したがって、この記事では「リアルタイム同期」という表現ではなく、Obsidianの予定をGoogleカレンダーへ自動反映する仕組みとして扱います。
Pythonの定期実行まで設定すれば、ICSファイルの更新処理も自動化できます。ただし、Googleカレンダー側の取得タイミングまで自分で制御できるわけではありません。
2.Googleカレンダーから予定を変更することはできない
今回の構成では、基本的にObsidianを予定管理のマスターとして扱います。
Obsidian(予定を登録・変更)
↓
Python(ICSを生成)
↓
Googleカレンダー(予定を確認)
という流れです。Googleカレンダー側で予定を変更しても、その変更がObsidianへ戻るわけではありません。そのため、双方向同期が必要な場合は別の方法を検討する必要があります。
3.ICSファイルを外部から取得できる必要がある
GoogleカレンダーからURLでカレンダーを追加するには、そのURLからカレンダーデータを取得できなければなりません。
Macのローカルフォルダに保存しただけでは、Googleカレンダーから取得できないため、外部からアクセスできるWebサーバーやストレージ環境を用意する必要があります。
4.予定情報の公開には注意する
外部から取得できるURLにICSを配置する以上、個人情報や機密情報(顧客名、電話番号、住所、パスワード等)を入れるのは避けてください。ブログ公開予定やSNS投稿予定など、外部に公開されても問題のない情報だけを配信する設計がおすすめです。
🛠️ 実装手順
ここから実際に仕組みを作っていきます。
全体の流れは3つのステップで完了します。
- Obsidianに予定表を作る
- PythonでICSファイルを生成する
- GoogleカレンダーにICSを登録する
1. Obsidianに予定表を作る
まず、Obsidianに予定を管理するMarkdownファイルを作成します。
例えば SNS投稿予定スケジュール.md というファイルを作ります。今回のコードでは、予定を次のようなMarkdownテーブルで管理します。
| ID | 予定日時 | ステータス | メモ |
|---|---|---|---|
| post-001 | 2026-08-25 18:00 | 予約済 | ObsidianとGoogleカレンダーの連携について |
| post-002 | 2026-08-26 08:00 | 未セット | コーヒーTDS測定の基本 |
| post-003 | 2026-08-27 18:00 | 未セット | AIエージェントの活用方法 |
今回のPythonプログラムでは、この表の ID・予定日時・ステータス・メモ を読み取ります。
IDを用意する理由
ここで重要なのが「ID」です。単純なプログラムでは「予定日時+予定名」などからUIDを生成することもできますが、それでは予定日時を変更しただけでUIDまで変わってしまいます。
iCalendarでは、UIDはイベントを継続的に識別するための永続的かつ一意な識別子として扱われます(RFC 5545)。そこでObsidian側で post-001 のような固定IDを持たせることで、日時を変更しても同じ予定として識別できるようになります。
日時の書き方
予定日時は YYYY-MM-DD HH:MM の形式で記述します。今回のコードでは日本時間(Asia/Tokyo)として読み込み、ICSではUTCへ変換して出力します。
2. PythonでICSファイルを生成する
次に、ObsidianのMarkdownファイルを読み取り、ICSファイルを生成するPythonプログラムを作ります。
外部ライブラリを使わず、Pythonの標準ライブラリだけで構成します。ファイル名は sync_calendar.py とします。
from pathlib import Path
from datetime import datetime, timezone, timedelta
from zoneinfo import ZoneInfo
import os
import re
# =========================
# 設定
# =========================
OBSIDIAN_FILE = Path(
"/Users/YOUR_USERNAME/Documents/Obsidian/SNS投稿予定スケジュール.md"
)
OUTPUT_ICS = Path(
"/Users/YOUR_USERNAME/Documents/calendar/schedule.ics"
)
SOURCE_TIMEZONE = ZoneInfo("Asia/Tokyo")
CALENDAR_NAME = "Obsidian予定表"
PRODID = "-//036Factory//Obsidian Calendar//JA"
# =========================
# ICS用エスケープ
# =========================
def escape_ics(value):
"""iCalendarのTEXT値をエスケープする。"""
value = str(value)
value = value.replace("\\", "\\\\")
value = value.replace(";", "\\;")
value = value.replace(",", "\\,")
value = value.replace("\r\n", "\\n")
value = value.replace("\n", "\\n")
value = value.replace("\r", "\\n")
return value
# =========================
# iCalendarの行折り返し
# =========================
def fold_ical_line(line, limit=75):
"""
iCalendarのcontent lineを75オクテット以内に折り返す。
UTF-8文字を途中で分割しない。
2行目以降には継続を示すスペースを付ける。
"""
if len(line.encode("utf-8")) <= limit:
return [line]
parts = []
current = ""
current_bytes = 0
first_line = True
for char in line:
char_bytes = len(char.encode("utf-8"))
max_bytes = limit if first_line else limit - 1
if current and current_bytes + char_bytes > max_bytes:
if first_line:
parts.append(current)
else:
parts.append(" " + current)
current = char
current_bytes = char_bytes
first_line = False
else:
current += char
current_bytes += char_bytes
if current:
if first_line:
parts.append(current)
else:
parts.append(" " + current)
return parts
# =========================
# Obsidianの予定を読み込む
# =========================
def parse_schedule(file_path):
events = []
if not file_path.exists():
raise FileNotFoundError(
f"Obsidianファイルが見つかりません: {file_path}"
)
text = file_path.read_text(encoding="utf-8")
pattern = re.compile(
r"^\|\s*"
r"([^|]+)"
r"\s*\|\s*"
r"(\d{4}[-/]\d{2}[-/]\d{2}\s+\d{2}:\d{2}(?::\d{2})?)"
r"\s*\|\s*"
r"([^|]+)"
r"\s*\|\s*"
r"([^|]+)"
r"\s*\|?\s*$"
)
for line in text.splitlines():
match = pattern.match(line.strip())
if not match:
continue
event_id = match.group(1).strip()
date_text = match.group(2).strip()
status = match.group(3).strip()
memo = match.group(4).strip()
# Markdownテーブルのヘッダー・区切り行を除外
if event_id.lower() == "id" or set(event_id) <= {"-", ":"}:
continue
start = None
formats = (
"%Y-%m-%d %H:%M",
"%Y/%m/%d %H:%M",
"%Y-%m-%d %H:%M:%S",
"%Y/%m/%d %H:%M:%S",
)
for fmt in formats:
try:
start = datetime.strptime(date_text, fmt)
break
except ValueError:
continue
if start is None:
print(f"日時を解釈できないためスキップします: {date_text}")
continue
# 日本時間として扱い、30分の予定として終了時刻を設定
start = start.replace(tzinfo=SOURCE_TIMEZONE)
end = start + timedelta(minutes=30)
events.append({
"id": event_id,
"start": start,
"end": end,
"status": status,
"memo": memo,
})
return events
# =========================
# UIDを作成
# =========================
def create_uid(event_id):
"""Obsidian側の固定IDを使ってUIDを作成する。"""
return f"{event_id}@036blog"
# =========================
# UTC形式へ変換
# =========================
def format_utc_datetime(dt):
"""datetimeをiCalendarのUTC形式へ変換する。"""
utc_dt = dt.astimezone(timezone.utc)
return utc_dt.strftime("%Y%m%dT%H%M%SZ")
# =========================
# ICSを生成
# =========================
def generate_ics(events, output_path):
lines = [
"BEGIN:VCALENDAR",
"VERSION:2.0",
f"PRODID:{PRODID}",
"CALSCALE:GREGORIAN",
"METHOD:PUBLISH",
f"X-WR-CALNAME:{escape_ics(CALENDAR_NAME)}",
]
now_utc = datetime.now(timezone.utc)
dtstamp = now_utc.strftime("%Y%m%dT%H%M%SZ")
for event in events:
uid = create_uid(event["id"])
start = format_utc_datetime(event["start"])
end = format_utc_datetime(event["end"])
summary = escape_ics(event["memo"])
description = escape_ics(
f"ステータス: {event['status']}\n"
f"メモ: {event['memo']}"
)
lines.extend([
"BEGIN:VEVENT",
f"UID:{escape_ics(uid)}",
f"DTSTAMP:{dtstamp}",
f"DTSTART:{start}",
f"DTEND:{end}",
f"SUMMARY:{summary}",
f"DESCRIPTION:{description}",
"END:VEVENT",
])
lines.append("END:VCALENDAR")
output_path.parent.mkdir(parents=True, exist_ok=True)
temp_path = output_path.with_suffix(output_path.suffix + ".tmp")
try:
with temp_path.open("w", encoding="utf-8", newline="") as file:
for line in lines:
folded_lines = fold_ical_line(line)
for folded_line in folded_lines:
file.write(folded_line + "\r\n")
os.replace(temp_path, output_path)
except Exception:
if temp_path.exists():
temp_path.unlink()
raise
# =========================
# 実行
# =========================
if __name__ == "__main__":
events = parse_schedule(OBSIDIAN_FILE)
generate_ics(events, OUTPUT_ICS)
print(f"{len(events)}件の予定をICSへ出力しました。")
なぜコードをこの形にしたのか(RFC 5545準拠の要点)
- UTF-8文字を途中で分割しない: 75文字ではなく75オクテット(バイト数)基準で文字単位分割。
- 継続行の先頭にスペースを入れる: 折り返し後の2行目以降の先頭に半角スペースを付与。
- CRLF(
\r\n)で出力: iCalendarのcontent line規格に準拠。 - UTCで日時を出力: 日本時間をUTC(末尾
Z)へ変換して出力し、VTIMEZONE定義を不要にするシンプルな構成。
パス変更と実行テスト
コード内の OBSIDIAN_FILE と OUTPUT_ICS を変更し、ターミナルで実行します。
python3 sync_calendar.py
正常に処理されれば schedule.ics が生成されます。
3. GoogleカレンダーにICSを登録する
生成したICSファイルを、外部からアクセスできるURL(例: https://example.com/calendar/schedule.ics)に配置します。
PCのブラウザからGoogleカレンダーを開きます。
- 左側にある 「他のカレンダー」 の横の「+」をクリックします。
- 「URLから」 を選択します。
- ICSファイルのURLを入力し、「カレンダーを追加」 をクリックします。
これで、Obsidianから生成した予定がGoogleカレンダー上に表示されます。
Obsidianの予定を変更した場合
Obsidian側で日時を変更(IDは固定のまま)し、再度 python3 sync_calendar.py を実行するとICSが更新されます。Googleカレンダー側がURLからデータを再取得したタイミングで反映されます。
※macOSの launchd やCronで定期実行を設定すれば、Pythonスクリプトの実行自体も完全自動化できます。
🤖 AIエージェントにPythonの実装を任せる
AIエージェントに実装を依頼する場合は、API方式ではなくICS方式およびRFC仕様を明示することが重要です。
ObsidianのMarkdownで管理している予定表から、
Googleカレンダーで購読できるICSファイルを
Pythonで自動生成する仕組みを構築してください。
条件:
・Google Calendar APIは使用しない
・OAuth認証は使用しない
・Python標準ライブラリを使用する
・ObsidianのMarkdownテーブルを読み取る
・ICS(iCalendar / RFC 5545)形式で出力する
・ICSのTEXT値を正しくエスケープする
・UTF-8文字を途中で分割しない
・iCalendarの75オクテット制限に対応する
・折り返した継続行の先頭にはスペースを付ける
・CRLFでICSを出力する
・Asia/Tokyoの日時をUTCへ変換して出力する
・DTSTARTとDTENDはUTC形式を使用する
・VEVENTごとにObsidian側の固定IDからUIDを生成する
・日時やタイトルを変更してもUIDが変わらない設計にする
・一時ファイルを使って安全にICSを更新する
・入力ファイルが存在しない場合はエラーを表示する
・日付を解釈できない行は安全にスキップする
・生成されたICSがRFC 5545の基本的な構文に沿っているか確認する
・テストデータを使って実際に動作確認する
Obsidianの予定ファイル:
[ここに絶対パス]
ICSの出力先:
[ここに絶対パス]
まず現在の環境を確認してください。
その後、Pythonスクリプトを作成し、
テストデータを使って実際に実行し、
ICSファイルが正常に生成されるところまで確認してください。
🛠️ 自力で実装するのが難しい、もっと効率化したい方へ
「自分のPC環境に合わせて自動化スクリプトを組むのが不安」
「ObsidianとSNS・ブログを連携した、より高度な知的生産システムを作りたい」
そう思われた方は、無理に一人で悩む必要はありません。
036Factoryでは、個人事業主や職人、発信者の方に向けて、**「AIエージェントを活用した業務自動化・ナレッジ管理システムの構築サポート」**を行っています。
- あなたのPC環境に合わせたObsidian自動化のセットアップ
- AIを活用したブログ・SNS投稿の半自動パイプライン構築
- 技術的なトラブルの個別相談・サポート
公式LINEにて、最新のAI活用ノウハウや自動化テンプレートを定期配信しています。お困りの際はお気軽にご相談ください。
👉 036Factory 公式LINEで自動化の相談をする(無料)
ICS方式のメリット・デメリット
| メリット | デメリット |
|---|---|
| Google Calendar APIが不要(Cloud設定・課金なし) | リアルタイム同期ではない(Google側の取得間隔に依存) |
| OAuth認証を実装しなくてよい(トークン切れなし) | 双方向同期ではない(Obsidian ➔ Googleの一方向) |
| 構成が比較的シンプルで壊れにくい | 外部から取得できるURLが必要 |
| Obsidianで予定を一元管理できる | iCalendar仕様(UTF-8/75バイト等)への対応が必要 |
Google Calendar APIを使ったほうがよいケース
次のような要件がある場合は、ICS方式よりGoogle Calendar APIが適しています。
- Googleカレンダーへプログラムから予定を作成・変更・削除したい
- GoogleカレンダーとObsidianを双方向に連携したい
- 分単位の即時反映(リアルタイム性)が必要
- 複数のユーザーが利用するWebシステムを作りたい
「Obsidianで管理している予定をGoogleカレンダーでも見たい」程度であれば、ICS方式が最も手軽で有力な選択肢になります。
ICS方式とGoogle Calendar APIの比較
| 項目 | ICS方式 | Google Calendar API |
|---|---|---|
| Google Calendar API | 不要 | 必要 |
| OAuth認証 | 不要 | 必要 |
| Google Cloud側のAPI設定 | 基本不要 | 必要 |
| Obsidian → Google | 可能 | 可能 |
| Google → Obsidian | 不可 | 実装可能 |
| 双方向連携 | 不可 | 可能 |
| リアルタイム性 | 低い | 要件に応じて設計可能 |
| 構築難易度 | 比較的低い | 高い |
| 個人利用 | 向いている | 要件次第 |
| 本格的なシステム | 不向き | 向いている |
よくある質問
ObsidianからGoogleカレンダーへ自動で反映できますか?
できます。ただし、今回の方法はリアルタイムの双方向同期ではありません。ObsidianをマスターとしてPythonでICSを更新し、Googleカレンダーから購読する構成です。Pythonを定期実行すれば更新処理まで自動化できます。
Google Calendar APIは必要ありませんか?
今回紹介したICS方式では必要ありません。Googleカレンダーの標準機能である外部カレンダー購読を利用するためです。
Googleカレンダーから予定を編集できますか?
今回の方法ではできません。Obsidian側で予定を変更し、PythonでICSを更新する必要があります。Googleカレンダーはこの構成では主に確認用として利用します。
Googleカレンダーへ登録したらすぐ反映されますか?
必ずしもそうとは限りません。Googleカレンダー側が外部カレンダーのデータを取得するタイミングによって、反映まで時間差が生じる可能性があります。
ICSファイルを手動でインポートする方法との違いは何ですか?
手動インポートはその時点のデータを取り込む静的取り込みのため、後からObsidianの予定を変更しても自動追従しません。URLから追加する方式であれば、継続的に更新を取得できます。
個人の予定をICSに入れても大丈夫ですか?
公開方法によっては注意が必要です。外部から取得できるURLに配置するため、本名・住所・顧客情報などの機密情報は含めず、公開しても問題のないスケジュールに限定してください。
なぜUIDをObsidian側で管理するのですか?
予定日時やタイトルを変更しても、同じイベントとして識別しやすくするためです。post-001 という固定IDが変わらなければ、日時を変更してもカレンダー側で二重登録にならず同一予定として正しく更新されます。
Pythonの標準ライブラリだけでICSを作るメリットは何ですか?
追加パッケージをインストールせずに動かせることです。コード内でUTF-8文字境界、75オクテット、折り返し、CRLF、エスケープ、UTC変換を明示的に処理しています。
まとめ
Obsidianの予定をGoogleカレンダーでも確認したい場合、必ずしもGoogle Calendar APIを使う必要はありません。
Obsidian → Python → ICS → Googleカレンダー
まずは、「予定はObsidianで管理する。確認はGoogleカレンダーで行う」というシンプルな役割分担から始めてみてください。
最初から複雑な仕組みを構築するのではなく、必要な機能に合わせて段階的に発展させることこそが、AIとツールを長く安定して運用するための重要な考え方です。