Chrome拡張機能でXブックマークをGoogle Sheetsに自動エクスポートする設計と実装
Xのブックマークが300件を超えたあたりで、過去に保存したツイートを探すのに毎回スクロールを繰り返していた。ブラウザのスクロールバーが米粒ほどに縮んでいくのを眺めながら、「これ全部CSVに抜けたら一発で検索できるのに」と手が動いた。会計ソフトA用に作ったChrome拡張のOAuth2認証パターンがそのまま使えそうだったので、流用する前提で設計を始めた。
設計フェーズ: 既存の認証パターンを流用する
会計ソフトA拡張のOAuth2パターン
会計ソフトA用に作ったChrome拡張では、chrome.identity.getAuthToken()でGoogleアカウントのOAuth2トークンを取得している。この仕組みをそのまま横展開する。
// chrome.identity.getAuthToken() で Google OAuth2 トークンを取得
const token = await chrome.identity.getAuthToken({
interactive: true,
scopes: ['https://www.googleapis.com/auth/spreadsheets']
});
manifest.jsonのoauth2セクションにスコープを追加するだけで、Sheets APIへのアクセス権が手に入る。Chrome拡張のOAuth2認証は、WebアプリのようにリダイレクトURIを設定する手間がなく、Chromeが認証フローを丸ごと管理してくれる。
全体アーキテクチャ
popup.html(UI)
↓ ユーザーが出力先を選択(CSV / Google Sheets)
content-script.js
↓ Xのブックマークページをスクレーピング
background.js
↓ データ整形 + 差分判定(tweet_idベース)
├→ CSV: Blob生成 → chrome.downloads
└→ Sheets: chrome.identity → Sheets API
Codexレビュー: 致命的指摘2点への対応
Codex CLIが動かない → codex-review-docスキルに切り替え
最初はcodex execコマンドで計画書を直接レビューしようとした。しかしCLIがトークン認証エラーで弾かれ、ターミナルにエラーが返ってくるだけだった。環境変数を確認しても問題なさそうで、ここで時間を溶かすのは避けたかった。codex-review-docスキル経由に切り替えたところ、すんなりレビューが通った。
指摘1: 認証設計の明記不足
Codexが「OAuth2認証フローの詳細が計画書に書かれていない。manifest.jsonのスコープ定義、トークンのリフレッシュ戦略、失効時のエラーハンドリングが抜けている」と指摘してきた。
確かに「chrome.identity使う」とだけ書いて、トークン失効時にどうするかを一行も書いていなかった。以下を計画書に追記した。
- manifest.jsonの
oauth2.scopesにSheets APIスコープを明記 - トークン失効時は
chrome.identity.removeCachedAuthToken()で古いトークンを破棄し、再取得を試みる - 再取得にも失敗した場合はポップアップにエラーメッセージを表示し、手動で再認証を促す
指摘2: 差分判定アルゴリズムの曖昧さ
「差分取得の基準がcreated_atなのかtweet_idなのか曖昧。タイムゾーン問題を考えるとcreated_atは罠が多い」という指摘。これは的を射ていた。
created_atはXのAPIがUTCで返すが、ユーザーのローカル時間と混在するとバグの温床になる。tweet_idはSnowflake IDで単調増加が保証されている。比較も数値の大小だけで済む。差分判定をtweet_idベースに統一した。
// 前回エクスポート時の最大tweet_idを保存
const lastExportedId = await chrome.storage.local.get('lastMaxTweetId');
// 新規ブックマークのみ抽出
const newBookmarks = allBookmarks.filter(
b => BigInt(b.tweet_id) > BigInt(lastExportedId || '0')
);
Phase 2: Google Sheets API連携
スプレッドシートの新規作成と既存シートへの差分追記
出力先として2つのモードを用意した。
- 新規作成モード: Sheets APIの
spreadsheets.createで新しいスプレッドシートを作り、ヘッダー行とデータを一括で書き込む - 追記モード: 既存のスプレッドシートIDを指定し、
spreadsheets.values.appendで末尾にデータを追加する
追記モードでは、前回エクスポート時のlastMaxTweetIdをchromeストレージに保存しておき、それより大きいIDのブックマークだけをappendする。
popup.htmlの出力先選択UI
ポップアップに出力先選択のラジオボタンを追加した。
- CSV: ローカルにCSVファイルをダウンロード。オフラインでも使える
- Google Sheets: 認証フローを経てスプレッドシートに出力。新規作成 or 既存シートIDを入力
Google Sheetsを選んだ場合のみ、スプレッドシートIDの入力欄が表示される。空欄なら新規作成、IDを入力すれば追記モードになる。
スクレーピング部分の設計メモ
Xのブックマークページは無限スクロールで読み込まれる。content-scriptでIntersectionObserverを使い、最下部に到達したら自動スクロールを繰り返す。各ツイートのDOM要素から以下を抽出する。
| フィールド | 取得方法 |
|---|---|
| tweet_id | ツイートリンクのURLパスから抽出 |
| author | ユーザー名のテキスト要素 |
| content | ツイート本文 |
| created_at | time要素のdatetime属性 |
| url | ツイートへのパーマリンク |
DOMの構造はXの更新で変わる可能性がある。セレクタが壊れたときに検知できるよう、抽出結果が0件のときは「セレクタが古い可能性があります」とポップアップに警告を出す設計にした。
試行錯誤の記録
うまくいったこと
- 会計ソフトA拡張のOAuth2パターンがほぼコピペで動いた。
chrome.identityの設計がChrome拡張同士でポータブルなのを体感した - Codexの指摘で差分判定を
tweet_idに統一したことで、タイムゾーン周りの複雑さが消えた
手間取ったこと
- Codex CLIのトークン認証エラーで15分ほど詰まった。結局codex-review-docスキル経由に逃げた形だが、原因は未調査のまま
- Xのブックマークページのセレクタ調査に時間がかかった。Reactのclass名がハッシュ化されていて、属性セレクタやdata属性を頼りに辿る必要があった
明日への引き継ぎ
Googleタスクに以下を登録した。
- content-scriptのスクレーピング部分を実装し、実際のXブックマークページで動作確認する
- Sheets APIへの書き込みをテスト用スプレッドシートで検証する
- エラーハンドリング(認証失効、スクレーピング失敗、API制限)のUI表示を作り込む
学びメモ
chrome.identity.getAuthToken()を2つ目のプロジェクトで使ったことで、Chrome拡張のOAuth2認証がプロジェクトを跨いで再利用できるパターンとして手元に定着した。manifest.jsonにスコープを足すだけでAPIアクセスが広がるのは、Webアプリのリダイレクト設定と比べると手数が半分以下で済む。
Codexレビューで「tweet_idに統一しろ」と言われたとき、最初は「created_atでも別にいけるだろう」と思った。しかし実際にタイムゾーン変換のコードを頭の中で組み立て始めた瞬間、UTCとJSTの変換ミスで重複取得するシナリオが3つ浮かんで、手が止まった。Snowflake IDの単調増加を信頼する方が、コードもテストもシンプルになる。レビューに救われた場面だった。