• #Chrome拡張機能
  • #X
  • #Google Sheets API
  • #OAuth2
  • #SQLite
  • #Mermaid
  • #データ分析
開発chrome-extension-xメモ

Chrome拡張機能: Xブックマーク分析基盤の構築

前日に設計・実装した X Bookmark Exporter を実際に動かし始めたら、OAuth2の認証画面が出ない。ターミナルにエラーが返り、ポップアップは無反応のまま固まっている。原因を追い始めたところ、client IDが別の拡張機能に紐付けられていた。ここから「動くまで持っていく」作業と、動いた後の「8,000件超のブックマークをどう料理するか」という分析作業が始まった。


OAuth2 client IDの罠: 拡張機能IDとの1対1紐付け

何が起きたか

会計ソフトA用Chrome拡張のOAuth2パターンを流用する設計だった。manifest.jsonのoauth2セクションをコピペして、スコープだけ変えれば動くはず――と思っていた。

しかしchrome.identity.getAuthToken()を呼んでも認証ダイアログが出ない。Chrome拡張のOAuth2では、Google Cloud Consoleで作成したclient IDが拡張機能ID(32文字の英小文字)と1対1で紐付く。会計ソフトA用のclient IDはあちらの拡張機能IDに結びついているので、別の拡張機能からは使えない。

解決: 新規client ID作成

Google Cloud Consoleで「Chrome拡張機能」タイプのOAuth 2.0クライアントIDを新規作成し、この拡張機能のIDを登録した。manifest.jsonのclient_idを差し替えたら、認証ダイアログが表示されてトークンが返ってきた。

{
  "oauth2": {
    "client_id": "xxxx-新規作成したID.apps.googleusercontent.com",
    "scopes": [
      "https://www.googleapis.com/auth/spreadsheets"
    ]
  }
}

教訓: chrome.identityのOAuth2パターン自体は再利用できるが、client IDはプロジェクトごとに別途作る必要がある。コードの流用とclient IDの流用は別物。


screen_name / display_nameの取得パス修正

X APIの構造変更に遭遇

ブックマークのスクレーピングでユーザー情報を取得する部分が、空文字を返してくる。DevToolsのNetworkタブでレスポンスのJSONを開いて目視で辿ったところ、以前は user_results.result.legacy にあったscreen_nameとdisplay_nameが、user_results.result.core 配下に移動していた。

// Before: legacy パスに格納されていた
const screenName = user.result.legacy.screen_name;

// After: core パスに移動
const screenName = user.result.core.user_results.result.legacy.screen_name;

X(旧Twitter)のAPI構造は予告なく変わる。セレクタやパスが壊れたときに「何が空なのか」をログに出す防御コードを追加した。


Sheets APIバッチ書き込み: 6,000件対応

500行ずつ分割して書き込む

ブックマークが数千件規模になると、Sheets APIの1回のリクエストに全行を詰め込むとタイムアウトする。500行ずつに分割してバッチ書き込みする方式にした。

const BATCH_SIZE = 500;
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
  const batch = rows.slice(i, i + BATCH_SIZE);
  await sheets.spreadsheets.values.append({
    spreadsheetId,
    range: 'Sheet1',
    valueInputOption: 'RAW',
    resource: { values: batch }
  });
}

6,000件のブックマークを12回のバッチで書き込めた。ポップアップには進捗表示(「500/6000件書き込み中...」)を追加して、処理が止まっていないことが目で見えるようにした。


UI改善: ブックマークページ以外からの実行対応

問題: ブックマークページを開いていないと動かない

当初の実装では、ユーザーがXのブックマークページ(x.com/i/bookmarks)を開いた状態でポップアップの実行ボタンを押す必要があった。他のページを見ているときに「あ、ブックマークをエクスポートしたい」と思ったら、まずブックマークページに手動で移動する手間がある。

解決: 自動でタブを開いて完了後に閉じる

ブックマークページ以外から実行ボタンを押した場合、background.jsが自動でブックマークページのタブを新規作成し、スクレーピング完了後にそのタブを閉じる処理を入れた。ユーザーの手元の作業が中断されない。

テスト用10件CSVボタン

開発中は毎回6,000件をスクレーピングすると時間がかかる。ポップアップに「テスト: 10件CSV出力」ボタンを一時的に追加し、スクレーピングの上限を10件に制限してCSVを吐くモードで動作確認を繰り返した。動作が安定したところでこのボタンは削除した。


background.jsのsendResponseエラー修正

Promiseのcatch漏れ

ポップアップからbackground.jsにメッセージを送り、非同期処理の結果をsendResponseで返すパターンで、コンソールにエラーが出ていた。

原因は、非同期処理をPromiseチェーンで書いていたが、.catch()でエラーを拾った際にsendResponseを呼んでいなかったこと。Chrome拡張のメッセージングでは、return true;で非同期応答を有効にした場合、全てのコードパスでsendResponseを呼ぶ必要がある。

// Before: catchでsendResponseを呼んでいない
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  doAsyncWork()
    .then(result => sendResponse({ success: true, data: result }))
    .catch(err => console.error(err)); // ← sendResponse漏れ
  return true;
});

// After: catchでもsendResponseを返す
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  doAsyncWork()
    .then(result => sendResponse({ success: true, data: result }))
    .catch(err => sendResponse({ success: false, error: err.message }));
  return true;
});

リネームとリファクタリング

拡張機能名の変更

動画ダウンロード機能とブックマークエクスポート機能の両方を持つようになったため、拡張機能名を「X Video & Bookmark Exporter」に変更した。

Simplifyリファクタリング

コードが膨らんできたので整理を入れた。

  • content.js: スクレーピングで使うCSSセレクタやAPI構造のパス文字列をファイル冒頭に定数として抽出
  • background.js: フォルダ管理用HTMLの重複テンプレートを共通関数に統合

ブックマーク8,251件をSQLiteに変換

CSV → SQLite変換

Sheets APIで出力したデータをCSVでも保存していたので、これをSQLiteに変換してbookmarks.dbを構築した。テーブルは1つで、tweet_id, author, screen_name, content, created_at, url, tagsカラムを持つ。

8,251件のブックマークが1つのDBファイルに収まった。SELECT * FROM bookmarks WHERE content LIKE '%Claude%'のようなクエリで即座に検索できる。スクロールを繰り返して目視で探していた頃とは比べものにならない。


年度別ブックマーク分析レポート: 9年分の興味関心の遷移

2018年〜2026年の年度別レポートを生成

bookmarks.dbに対して年度ごとの集計クエリを走らせ、各年のブックマーク傾向をMermaid図付きでレポートにまとめた。9年分の年度別レポートを一気に生成した。

2023年を境に変わる景色

データを並べると、2023年以前と以降で明らかにブックマークの傾向が変わっている。

  • 2018〜2022年: プログラミング全般、Web開発、会計・税務、雑多な技術情報
  • 2023年〜: ChatGPTの登場以降、AI・LLM・プロンプトエンジニアリング関連のブックマークが急増

2022年まではカテゴリが均等に散らばっていたのが、2023年を境にAI系タグがブックマーク全体の4割を超えた。自分では「まんべんなく情報を追っている」つもりだったが、数字は正直だった。興味関心がAIに吸い寄せられていく軌跡が、ブックマークのタイムスタンプに刻まれていた。


学びメモ

Chrome拡張のOAuth2 client IDが拡張機能IDと1対1で紐付く仕組みは、ドキュメントを読んでも頭に入らなかった。会計ソフトA拡張のclient IDをコピペして認証画面が出ない状況に15分ほど向き合い、Cloud Consoleの設定画面を開いて「あ、ここにアプリケーションIDが登録されている」と目で確認して初めて理解した。

8,251件のブックマークをSQLiteに入れて年度別に集計したとき、ターミナルに数字が並ぶのを眺めて手が止まった。自分の9年間の関心の流れが、GROUP BYの結果として返ってくる。「2023年にChatGPTに出会って、そこからAIに没頭した」という自覚はあったが、ブックマーク数の推移グラフで裏付けられると、なんというか、動かぬ証拠を突きつけられた気分になる。