本記事は生成AIと共同で執筆しています。事実関係は可能な範囲で公式ドキュメント等と照合していますが、誤りが含まれている可能性があります。重要な判断を行う前にご自身でも一次情報をご確認ください。

調整さん の出欠回答を毎回手で入れるのが面倒だったので、Playwright で自動入力する CLI を作りました。候補日それぞれの / / × の判定は Google カレンダーと照合する必要があるため、その部分は Claude Code(claude.ai Google Calendar MCP 経由)に任せる構成にしています。

ソースコード: https://github.com/nakamura196/choseisan

TL;DR

  • 調整さんは公開 API を持たないので、Playwright で公開ページの DOM を直接操作している。
  • 入力は3段階のワークフローに分けた: fetch で候補日を抽出、Claude にカレンダー照合と //× 埋めを依頼、submit で送信。
  • 判定ルール(参考: プレフィックスのスキップ、透過設定の扱い、終日イベントのブロック等)は CLAUDE.md に記述し、Claude Code がディレクトリで作業するときに自動的に読み込まれるようにした。
  • 既存エントリの編集(同じ名前で再度登録した場合)は、参加者名のリンクをクリックするモードで自動切替する。
  • wait_for_load_state("networkidle") は調整さんのページ(広告タグ多数)では永遠に終わらないので "load" を使う。

構成

3段階を独立に再実行できる構成にしました。

fetch <url>      events/<slug>.json テンプレ生成
(Claude に依頼)  Google カレンダー照合で answer を埋める
submit <slug>    Playwright で送信(新規/更新を自動判定)

利点は、候補が変わったら fetch だけ再実行、判定ロジックを試したいなら Claude に再依頼するだけ、確認後に submit、という形で各段階を切り離せることです。

ファイル構成は以下に落ち着きました。

パス役割
cli.pyサブコマンド fetch / list / submit
CLAUDE.mdClaude Code 向けの判定ルール
events/<slug>.jsonイベント単位の設定+出欠(gitignore)
.envCHOUSEISAN_NAME(fetch 時の既定氏名、gitignore)

調整さんの DOM 観測

公開 API が無いので、まず DOM を観察する必要があります。Playwright で対象ページを取得し、入力フォーム部分の HTML をダンプして主要な要素を特定しました。

入力フォームは「出欠を入力する」ボタンをクリックすると展開される作りです。展開後のフォーム要素をいくつか並べると、構造が見えてきます。

<input type="button" id="add_btn" class="add-button-input" value="出欠を入力する">

<input id="f_name" class="form-input" maxlength="50" name="name" aria-label="名前">

<!-- 候補ごとに hidden + 3つの画像ボタン -->
<input type="hidden" name="kouho1" id="kouho1" value="3">
<input type="image" class="oax oax-0" id="oax_0_0" alt="5/7(木) 10:00〜11:00_まる">
<input type="image" class="oax oax-1" id="oax_0_1" alt="5/7(木) 10:00〜11:00_さんかく">
<input type="image" class="oax oax-2 active" id="oax_0_2" alt="5/7(木) 10:00〜11:00_ばつ" aria-pressed="true">

<input class="form-input hitokoto-input" maxlength="500" name="hitokoto" aria-label="コメント" type="text">

<input type="submit" name="add" id="memUpdBtn" value="入力する">

知りたかった点は3つです。

  1. 候補ごとの値はどこに保存されるかinput#kouho{N} という hidden input に value として 1 / 2 / 3 のいずれかが入る(1=◯、2=△、3=×、初期値 3)。
  2. クリック対象は何か → 同じ <td> 内に並ぶ input[type="image"].oax-{0,1,2} の画像ボタン。oax-0 が「まる」、oax-1 が「さんかく」、oax-2 が「ばつ」。クリックすると hidden input の値も連動する。
  3. 行をどう特定するかid="oax_0_0" のような ID は行をまたいで重複していたため、ID は使わず、#kouho{N} を起点に CSS の sibling combinator (~) で対応する画像ボタンを取りに行く形にした。
# N番目の行で col 列のボタンをクリック
# col: 0=◯, 1=△, 2=×
btn = page.locator(f"#kouho{idx} ~ input.oax-{col}").first
btn.scroll_into_view_if_needed()
btn.click()

#kouho1 ~ input.oax-0 は「#kouho1 の後続兄弟のうち、クラス oax-0 を持つ input」というセレクタです。同じ <td> 内に並んでいるため、これで一意に取れます。

CLI

サブコマンドは3つだけです。

# fetch: URL から候補日を抽出して events/<slug>.json テンプレを作成
python cli.py fetch https://chouseisan.com/s?h=<ハッシュ>

# list: 登録済みイベント一覧
python cli.py list
#   abc123def456  ○○年度初回ミーティング  43件 ◯18 △2 ×23  [中村太郎]

# submit: events/<slug>.json を読んで送信
python cli.py submit <slug> --dry-run    # 入力までして送信せず終了
python cli.py submit <slug>              # 本番送信
python cli.py submit <slug> --debug      # ブラウザ表示+pause(DOM確認)

fetch の出力 JSON:

{
  "slug": "abc123def456",
  "label": "○○年度初回ミーティング",
  "url": "https://chouseisan.com/s?h=...",
  "name": "中村太郎",
  "comment": "",
  "choices": [
    { "date": "5/7(木) 10:00〜11:00", "answer": "×" },
    { "date": "5/7(木) 11:00〜12:00", "answer": "×" }
  ]
}

初期値は全て ×。これを Claude に依頼して埋めてもらいます。

Google Calendar MCP との連携

判定ロジックを CLI 内に書くか、別の AI コール(Anthropic SDK 経由)でやるか迷ったのですが、最終的には Claude Code セッションに対話で頼む 方式に落ち着きました。

理由は、Claude.ai の Google Calendar MCP を使うと、OAuth クライアント作成や token.json の管理を自分でやらずに済むためです。Claude Code 側で /mcp から接続を認証すれば、それ以降は対話の中で list_events 相当のツールが使えます。

実際の依頼はこんな形です。

b03_2026 のスケジュールを Google カレンダーと照合して埋めて

これだけで、Claude が events/b03_2026.jsonchoices に並んだ候補日時帯を見て、対応する Google カレンダーのイベントを取得し、衝突するものは × に、空きは に、直前直後にバッファゼロで他予定があるものは に、と書き換えてくれます。

返ってきた JSON を見て、5/14 教授会(終日)→ 朝候補は × のような判定が一覧で表示されるので、必要なら「5/14 朝は実は OK」のように指示して再判定させます。判定が確定したら submit を走らせる、という流れです。

判定ルールは CLAUDE.md に書く

利用者ごとにカレンダーの使い方には癖があります。私の場合は、

  • summary参考: で始まるイベントは情報共有用で、実際には時間を拘束しない
  • summary終日: で始まるものは終日イベントの明示ラベル
  • transparency: transparent のイベントは Google カレンダー上「予定なし」扱いなので空きとみなす
  • 終日イベントのうち、上記に当てはまらないものは終日ブロック

といった命名規則・判断ルールがあるので、これらをプロジェクトルートの CLAUDE.md に書いておきます。Claude Code は作業ディレクトリの CLAUDE.md を自動的に読むため、毎回ルールを伝えなおす必要がありません。

### スキップする予定

`summary` が以下のプレフィックスで始まる予定はブロッキング扱いせずスキップする:

- `参考:` … 情報共有用の参考情報。実際は時間を拘束しない

### ブロッキング扱いする予定

- 時間指定イベント(`dateTime` を持つ)
- 終日イベント(`date` を持つ)で `transparency``transparent` でないもの
- `summary``終日:` で始まる予定

### 答えのマッピング

| 状況 | answer |
|---|---|
| 候補時間に重なる予定なし | `◯` |
| 直前/直後にバッファゼロで他予定 | `△` |
| 候補時間と重なる予定あり | `×` |
| 終日ブロッキング予定がある日 | `×` |

ルールを別ファイルに切り出すことの一番のメリットは、判定が変だったときに 「ルールがおかしかった」のか「Claude が解釈を誤った」のかの切り分けがしやすい ことだと感じています。

既存エントリの更新サポート

調整さんでは「各自の出欠状況を変更するには名前のリンクをクリックしてください」とページ上に書かれているとおり、既に登録した人が再編集する場合は、#add_btn ではなく 参加者一覧の自分の名前をクリック してフォームを開く UI になっています。

CLI 側で同じ JSON で submit を再実行したときに、毎回新規エントリが増えてしまうのは困るので、ページ表示直後に 同名の参加者リンクが既にあるかを確認 し、あれば編集モード、なければ新規モードに切り替えるようにしました。

def _existing_entry_link(page, name):
    link = page.get_by_role("link", name=name, exact=True)
    return link.first if link.count() > 0 else None


def _open_form(page, name):
    link = _existing_entry_link(page, name)
    if link is not None:
        link.click()
        page.wait_for_selector("#f_name", state="visible", timeout=5000)
        return "update"
    page.locator("#add_btn").click()
    page.wait_for_selector("#f_name", state="visible", timeout=5000)
    return "new"

実行時にどちらのモードかを表示しています。

イベント: B03班2026年度初回ミーティング
名前: 中村太郎  (モード: 更新)
  [ 1/43] 5/7(木) 10:00〜11:00 → ◯
  ...

ハマったところ

networkidle で待つと終わらない

最初は送信後に page.wait_for_load_state("networkidle") で待っていたのですが、調整さんのページは Google Ads などの第三者タグが多数読み込まれており、networkidle 状態が30秒経っても来ないことがありました。送信自体は domcontentloaded の直後に成立しているので、"load" で十分です。

page.wait_for_load_state("load", timeout=15000)

エラーで例外が飛んだ後、別のセッションで実際にページを確認したら登録は完了していた、という状況です。

oax_0_0 の ID が行をまたいで重複している

最初は #oax_{row}_{col} の形で行番号付きの ID が振られていることを期待したのですが、観測した HTML では全行の最初の画像ボタンが id="oax_0_0" になっていました。Playwright の locator(f"#oax_{idx}_{col}") だと strict mode 違反になります。#kouho{N} の hidden input は ID に通し番号が入っていたので、これを起点に ~ で後続兄弟を取る形に変更しました。

まとめ

  • 調整さんの DOM は Vue ベースですが、フォーム自体は普通の HTML フォームでサーバ提出する方式なので、Playwright での自動化は素直です。
  • 判定の AI パートを「Claude Code セッションに対話で頼む」形にすると、API キー管理や OAuth 設定なしで Google カレンダーと連携できます。MCP(Model Context Protocol)の使い方として手軽な部類かと思います。
  • ルールを CLAUDE.md に書いて版管理できる形にすることで、利用者ごとの判断基準(プレフィックス命名、終日設定の意味)を AI に教える手段がスクリプト外に出せます。

ソースコードは https://github.com/nakamura196/choseisan に置いています。

留保

  • 調整さんの DOM 構造は予告なく変わる可能性があります。本記事のセレクタ情報は2026年4月時点での観測に基づきます。
  • 調整さんは個人の予定調整ツールです。本ツールは「自分の1票を入力する手間を省く」用途を想定しており、大量送信や高頻度のクロール用途は意図していません。
  • 本記事および関連リポジトリは個人の有志プロジェクトであり、ミクステンド株式会社(調整さん運営元)とは無関係です。

ソース