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に没頭した」という自覚はあったが、ブックマーク数の推移グラフで裏付けられると、なんというか、動かぬ証拠を突きつけられた気分になる。