今年のお盆休みはどこにも行かず、家でひたすら慣れないツール作りに励んでいた。ChatGPTに相談しつつ、失敗を繰り返しながらの作業。時折見せるChatGPTのポンコツっぷりにイラつくも、私のプロンプトが悪いのは明白。アレコレうるさいオッサンを宥めるのに、相当苦労したんじゃないかな。ごめんな、GPT。
今回作りたかったのは、ChatGPTからGoogleカレンダーに予定を入力するツールと、Googleカレンダーに登録された予定をObsidianのDailyノートに自動で追加するツール。結局前者は失敗し、今もそのプロジェクトは続いている。後者は成功し、安定して動いている。以下、どのような構成で、どのような苦労があったかを備忘録的にまとめておく。
1. 目的と要件
- 入力(入口)は Googleカレンダー:iPhone/職場Mac/自宅Mac から共通で入れやすい。
- 記録(出口)は ObsidianのDailyノート:当日の出来事ログを自動追記。
- 終日と時間ありを区別。時間ありは
HH:MM-HH:MM表記、終日は(終日)を付与。 - Zapierは「1イベント=1 Markdownファイル」をGoogle Driveに作成(Appendが見つからなかった)。Mac側で取り込み後に
archive/へ移動。
2. 全体構成
- Zapier:Google Calendar → Google Drive(1イベント→1つの
.md) - macOS LaunchAgent:Google Drive の受け皿フォルダを監視し、変化で Python 起動
- Python:Markdownから「タイトル」「開始・終了(または終日)」を抽出し、Obsidianの
Daily/YYYY/MM/YYYY-MM-DD.mdに追記 → 処理済みの.mdファイルはarchive/移動
ossan@example.com、/Users/ossan/...
3. Zapier(Googleカレンダー → Google Drive)
3.1 トリガ
- Google Calendar:New or Updated Eventでテスト。
- 取得フィールド例:
Summary(件名)、Start Date Time、End Date Time、All-day。
3.2 変換(任意)
- Formatter(Date/Time):Zapier側の選択肢で、ISOのままでも後段Pythonで整形可能。
3.3 出力
- Google Drive:Upload/Create File(1イベント=1ファイル)。
- ファイル名例:
2025-08-17_頭を剃る.md - 本文例(Zapierのテンプレ内):
- [ ] 頭を剃る 2025-08-17T17:00:00+09:00から2025-08-17T18:00:00+09:00
4. フォルダ監視(launchd, LaunchAgent)
ユーザセッションで動かすため ~/Library/LaunchAgents/ に plist を配置。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.ossan.pullgcal.watch</string>
<key>ProgramArguments</key>
<array>
<string>/Library/Frameworks/Python.framework/Versions/3.13/bin/python3</string>
<string>/Users/ossan/bin/pull_gcal_feed.py</string>
</array>
<key>WorkingDirectory</key>
<string>/Users/ossan/bin</string>
<key>StandardOutPath</key><string>/tmp/pull_gcal.out</string>
<key>StandardErrorPath</key><string>/tmp/pull_gcal.err</string>
<key>WatchPaths</key>
<array>
<string>/Users/ossan/Library/CloudStorage/GoogleDrive-foo@example.com/マイドライブ/ZapierCalendarFeed</string>
</array>
<key>RunAtLoad</key><true/>
<key>ThrottleInterval</key><integer>10</integer>
</dict>
</plist>
有効化:launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.ossan.pullgcal.watch.plist
更新時:launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.ossan.pullgcal.watch.plist → bootstrap
状態確認:launchctl print gui/$(id -u)/com.ossan.pullgcal.watch
5. Python(抽出・整形・追記・アーカイブ)
5.1 入口・出口のパス
from pathlib import Path
import re, shutil
from datetime import datetime
from zoneinfo import ZoneInfo
TZ = ZoneInfo("Asia/Tokyo")
INBOX = Path("/Users/ossan/Library/CloudStorage/GoogleDrive-foo@example.com/マイドライブ/ZapierCalendarFeed")
ARCH = INBOX / "archive"
DAILY = Path("/Volumes/knowledge/Obsidian/2nd brain/daily")
5.2 イベント行の抽出(時間あり/終日)
ISO_RANGE_RE = re.compile(
r"^- \[\s?\]\s*(?P<title>.*?)\s+"
r"(?P<s>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:[+\-]\d{2}:\d{2})?)から"
r"(?P<e>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:[+\-]\d{2}:\d{2})?)$"
)
ALLDAY_RE = re.compile(
r"^- \[\s?\]\s*(?P<title>.*?)\s+(?P<d>\d{4}-\d{2}-\d{2})$"
)
def parse_dt(s: str) -> datetime:
# 例: 2025-08-17T17:00:00+09:00
return datetime.fromisoformat(s)
5.3 ノーマライズ(表示文言の生成)
def normalize_line(raw: str):
m = ISO_RANGE_RE.search(raw)
if m:
title = m.group("title").strip()
s = parse_dt(m.group("s")).astimezone(TZ)
e = parse_dt(m.group("e")).astimezone(TZ)
day = s.strftime("%Y-%m-%d")
text = f"- [ ] {title} {s:%H:%M}-{e:%H:%M}"
return day, text
m2 = ALLDAY_RE.search(raw)
if m2:
title = m2.group("title").strip()
day = m2.group("d")
text = f"- [ ] {title} (終日)"
return day, text
return None, None
5.4 Dailyノートに追記(自動で年月フォルダを作成)
def append_to_daily(day: str, line: str):
yyyy = day[:4]
mm = day[5:7]
dest_dir = DAILY / yyyy / mm
dest_dir.mkdir(parents=True, exist_ok=True)
dest = dest_dir / f"{day}.md"
with dest.open("a", encoding="utf-8") as f:
f.write(line + "\n")
return dest
5.5 受け皿の .md を取り込み → archive へ移動
def process_inbox():
ARCH.mkdir(exist_ok=True)
processed = 0
for p in sorted(INBOX.glob("*.md")):
try:
raw = p.read_text(encoding="utf-8").strip()
day, line = normalize_line(raw)
if day and line:
dest = append_to_daily(day, line)
print(f"[OK] {p.name} → {dest}")
shutil.move(str(p), str(ARCH / p.name))
processed += 1
else:
print(f"[SKIP] {p.name}(フォーマット不一致)")
except Exception as e:
print(f"[ERR] {p.name}: {e}")
print(f"[DONE] processed={processed}")
LaunchAgent からは pull_gcal_feed.py の if __name__ == "__main__": process_inbox() を叩くだけ。
6. 運用とログ
- 受け皿にファイルが生成されると、数秒〜十数秒で取り込み → Daily追記 →
archive/移動。 - ログ:
/tmp/pull_gcal.out(正常系・進捗)、/tmp/pull_gcal.err(例外など)。 - 重複はZapier側で同一イベントの多重発火が無ければ概ね起きない。起きた場合は受け皿に来るファイルが複製されていないか確認。
7. 失敗談:Timebox直書き同期の壁
Obsidianの timebox.md に #timebox や start/due を記入 → Python で Google Calendar API へ同期するアプローチも試した。重複回避に extendedProperties.private.fingerprint を持たせてハッシュ管理したが、次の点で運用が重くなった。
- 複数端末・iPhoneからの入力は結局「googleカレンダー」のほうが速い(笑)
- Obsidian→Google の「片側主従」は破綻しやすい(現場で予定を変える起点がGoogle側にある)。
結論として、「入口=Googleカレンダー、記録=Obsidian」に役割分担したほうがシンプルで安定した。
8. 技術的な課題と対処
- ZapierのAppend不在:1イベント=1ファイルを生成し、Mac側で吸収・追記・アーカイブする方式で解決。
- Google Driveの同期遅延:ストリーミング(ファイルオンデマンド)でも数秒の遅延は起こり得る。LaunchAgentの
ThrottleIntervalを短くしすぎると無駄起動が増えるため注意する。 - macOS権限:フルディスクアクセス・ログイン時起動の権限漏れで失敗しやすい。LaunchAgentのログをまず確認。
9. 未達成:ChatGPT → Googleカレンダー直接登録
「自然文(例:『来週水曜の午後に30分、仕事の計画』)→ 予定作成」を目指しているが、API連携の方式・権限・審査など現時点の自分の環境では未整備。ChatGPT側の問題もあるのではないかと考察しているが、詳細は不明。そのうちに正式に対応してくれるのを待つのもいいかも。
10. まとめ
- 入力はGoogleカレンダー、記録はObsidianに集約する設計が現実的。
- Zapierで「1イベント=1ファイル」→ LaunchAgent監視 → Pythonで整形・追記・アーカイブ、の三段構えで安定。
- 相当疲れた。Zapierは相当便利だが、慣れるまでちょっと戸惑った。
- ChatGPTは素晴らしいが、プロンプトを打つオッサンがヘボいと、余計な回り道を強いられる。プロンプトの研究が必要。