最終更新: 2026-04-28
はじめに
Web自動化で一番時間を溶かしたのは、セッション切れだった。
note 自動投稿・BSky エンゲージ・Gemini Web 操作——この3本だけで、1週間に平均4〜5回は「ログインしてください」が出ていた。スクリプトを走らせる前にターミナルを確認して、ログインエラーを見て、手動でブラウザを開いて……その一連が毎朝の儀式になっていた時期がある。
最初は storage_state で対処しようとした。Playwright の公式アプローチで、ログイン済みのセッションをJSONに保存して使い回す方法だ。2週間は動いた。3週目に note のクッキー有効期限が切れて振り出し。Amazon は別のトークン体系で storage_state 自体が信頼できなかった。
で、私はこっちに倒した。日常使いしてる Brave ブラウザに CDP(Chrome DevTools Protocol)で接続して、そのセッションをそのまま流用する。
一回ログインしてしまえば、あとは自動化スクリプトが「すでにログイン済みのブラウザ」として動く。セッション切れが起きても Brave 上で手動ログインするだけで即復旧——これがめちゃくちゃ楽だった。
ただ、ここに辿り着くまでにそれなりにハマった。今日はその記録。
CDP 接続の仕組みをざっくり理解する
CDP は Chromium プロジェクトが提供するデバッグ用プロトコルで、外部からブラウザを操作できるようにする仕組み。本来は Chrome DevTools がブラウザと通信するために使っているもので、Chrome DevTools Protocol の仕様は公式で公開されている。
Playwright はこれを使って既存のブラウザプロセスに「接続」できる。新しくブラウザを起動するんじゃなくて、すでに動いているブラウザに後から乗り込む感じ。Playwright 公式ドキュメントの connect_over_cdp に詳細がある。
Brave は Chromium ベースなので CDP がそのまま使える。起動時に --remote-debugging-port=9222 を渡してやれば、localhost:9222 でデバッグポートが開く。
Brave をデバッグモードで起動する
まずここから。通常の起動方法だと CDP は有効にならないので、専用のシェルスクリプトを用意した。
[コード略 — 詳細は元記事参照]
これを実行して Brave が立ち上がったら、ブラウザ上で手動ログインを済ませておく。note・X・Amazon・Gemini Web など、スクリプトが操作したいサービスを全部片付ける。初回だけ10分ほどかかるが、以後は Brave を起動するだけでいい。
疎通確認はこれ一行でできる。
[コード略 — 詳細は元記事参照]
"Browser": "Brave/..." みたいなレスポンスが返ってきたら OK。ここで何も返ってこないか ConnectionRefusedError が出る場合は、Brave がデバッグポートなしで通常起動している。一度終了して上のスクリプトで再起動する。
cdp_browser.py を作った
プロジェクト内で現在5本のスクリプトが CDP 接続を使っている。最初は各スクリプトに接続コードを書いていたが、ポート番号や待機処理を何箇所も直すのが面倒になって、共通ライブラリとして切り出した。これが 01_Scripts/lib/cdp_browser.py。
[コード略 — 詳細は元記事参照]
Python の asyncio を使った async 版も
同様に用意してある。connect_cdp_async と open_tab_async の2つ。BSky エンゲージは非同期で複数タブを並列操作するので、そちらを使っている。
使い方はシンプルで、各スクリプトでこう書くだけ。
[コード略 — 詳細は元記事参照]
browser.close() は CDP 接続を切るだけで Brave 本体は落ちない。これ、最初ちゃんと理解できていなかった。「スクリプトが終わるたびに Brave が落ちる気がする」と2日間悩んで、実は別の原因だったと気づいた。CDP 接続を閉じることとプロセスを終了することは完全に別の操作——腹落ちしてからは混乱がなくなった。
ハマったポイント3つ
1. contexts が空のときがある
Brave を起動したばかりで何もタブを開いていないと、browser.contexts が空のリストになることがある。contexts[0] で IndexError: list index out of range が出て気づいた。
コード上は if browser.contexts else browser.new_context() でフォールバックしているが、実用上は Brave 起動後に少なくとも1タブ開いておく運用にしている。エラーで気づく前に「なぜか最初の1回だけ失敗する」という症状が3日続いていた。
2. ポート番号の競合
--remote-debugging-port=9222 は固定で使っているが、何かの拍子に Brave が落ちて再起動したとき、ゾンビプロセスがポートを掴んだままになることがあった。この状態だと Playwright 側が接続できず、エラーメッセージも「接続先がない」系で紛らわしい。原因特定だけで30分近くかかった。
[コード略 — 詳細は元記事参照]
で確認して、PID を kill してから再起動するのが手順として定着した。
3. browser.close() で Brave が落ちると思ってた
「スクリプト終了 → Brave が落ちた」という因果関係を疑い続けていた時期があった。実際に Brave が落ちていたのは、長時間デバッグポートを掴んだままにしていたため macOS がリソース回収したケースだった。CDP 接続を閉じるのとブラウザプロセスを終了するのは別の話——これをちゃんと理解できたのが大きかった。
launchd に組み込んでいる
自動化スクリプトは launchd で定期実行しているので、Brave の起動も launchd で管理するか迷ったけど、今は手動起動にしている。
理由は単純で、Brave は日常的に使うブラウザだから、Mac を起動したら普通に開く。launchd 経由で起動すると、Brave のプロファイルがどのユーザーセッションで起動するかで挙動が変わることがわかって、ログイン状態の管理が複雑になる。手動ログインのフローを残したほうが現実的だった。
スクリプト側は CDP が繋がらないときに「Brave がデバッグモードで起動していません」と Discord 通知を飛ばすようにしてある。通知が来たら Brave を再起動するだけ——月に2〜3回あるかないか、というペースに落ち着いている。
[コード略 — 詳細は元記事参照]
現在の運用状況
今は note 自動投稿・BSky エンゲージ・KDP 関連の Gemini Web 操作・Amazon の A+ コンテンツ確認——この4系統が全部この CDP 接続経由で動っている。セッション切れで詰まることがほぼなくなった。以前は週4〜5回あったログインエラーが、今は月2〜3回程度。
cdp_browser.py に集約してから、新しいスクリプトを書くときの実装コストもかなり下がった。接続部分を毎回書かなくていい。新しいサービスの自動化を追加するときも、ログイン済みの Brave で手動確認 → スクリプト化、という流れが確立している。
まとめ
CDP 接続は、ログイン状態を維持したいケースにかなり効く。新規ブラウザ起動 + storage_state の管理をやめてから、週4〜5回あった自動化のトラブルが月2〜3回まで落ちた。
Brave を --remote-debugging-port=9222 で起動 → 手動ログイン → cdp_browser.py 経由で接続。流れとしてはこれだけ。シンプルだけど、storage_state の落とし穴・ポート競合・browser.close() の誤解、この3つで結構な時間を溶かした。同じところで詰まっている人の参考になれば。✨
[コード略 — 詳細は元記事参照]