Chrome拡張機能にGoogle Sheets API書き込みを追加する
会計ソフトの明細を自動取得するChrome拡張を以前から使っていたが、出力はCSVダウンロードだけだった。取得したデータをそのままGoogle Sheetsに書き込めれば、ダウンロード→手動インポートの手間が消える。OAuth2認証の設定で2回つまずき、API URLで1回つまずき、差分追記のロジックを組んで、最後にページ遷移の待機を直した。半日の作業ログ。
OAuth2認証: クライアントIDのタイプを間違えた
Chrome拡張からGoogle APIを叩くには、chrome.identity.getAuthToken() でOAuth2トークンを取得する。manifest.jsonに oauth2 セクションを追加して、Google Cloud ConsoleのクライアントIDを設定するだけ――のはずだった。
最初のエラー: bad client id
既存のデスクトップアプリ用クライアントIDをそのまま使ったら、getAuthToken() が bad client id を返した。
原因はクライアントIDのタイプ。Chrome拡張向けには「Chrome アプリ」タイプで新規作成する必要がある。Google Cloud Consoleの認証情報画面で「OAuth 2.0 クライアント ID の作成」→ アプリケーションの種類で「Chrome アプリ」を選び、拡張機能のIDを登録する。
// manifest.json
{
"oauth2": {
"client_id": "xxxx.apps.googleusercontent.com",
"scopes": ["https://www.googleapis.com/auth/spreadsheets"]
}
}
「Chrome アプリ」タイプを選ぶと、クライアントIDがChrome拡張のIDと紐づく。デスクトップ用やWeb用のクライアントIDではこの紐づけが存在しないため、chrome.identity がIDを弾く。
host_permissions漏れ: Codexレビューで発覚
manifest.jsonにoauth2セクションを追加した段階で、Codexにレビューを投げた。返ってきた指摘の1つが host_permissions の漏れだった。
{
"host_permissions": [
"https://www.googleapis.com/*"
]
}
Chrome拡張のservice workerから外部APIへ fetch するには、host_permissions にそのドメインを明示的に追加しなければならない。content scriptからのリクエストとは権限モデルが異なる。これが漏れるとfetchがブロックされて無言で失敗する。
Codexの指摘がなければ、実行時に「なぜか通信できない」と頭を抱えていたはず。レビューを挟む価値を実感した瞬間だった。
API URL: googleapis.com の罠
認証が通り、host_permissionsも設定した。いざシートに書き込もうとして、今度は404が返ってきた。
原因はベースURL。最初に書いたコードでは https://www.googleapis.com/v4/spreadsheets/... を叩いていたが、Sheets API v4の正しいベースURLは https://sheets.googleapis.com/v4/spreadsheets/... だった。
// NG: 404
const url = `https://www.googleapis.com/v4/spreadsheets/${sheetId}/values/${range}`;
// OK
const url = `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}/values/${range}`;
Google APIはサービスごとにサブドメインが異なる。www.googleapis.com はDiscovery APIなど一部のエンドポイントで使われるが、Sheets APIは sheets.googleapis.com に住んでいる。公式ドキュメントのサンプルコードをコピーすれば間違えないが、記憶で書くとこういう罠を踏む。
host_permissionsにも https://sheets.googleapis.com/* を追加して解決した。
差分追記: 全上書きではなくdiffで追記する
APIが通ったので、次は書き込みロジック。最初は「毎回全データを上書き」で実装しようとしたが、Google Sheetsのセル編集履歴が全て吹き飛ぶことに気づいて方針を変えた。手動でメモを追記していたセルがあったため、全上書きは使えない。
実装方針
- 既存データを
spreadsheets.values.getで取得 - 新規データの各行について、既存行と全列のJSON.stringify一致で重複判定
- 既存に存在しない行だけを
spreadsheets.values.appendで追記
const isDuplicate = (newRow, existingRows) =>
existingRows.some(existing =>
JSON.stringify(existing) === JSON.stringify(newRow)
);
const rowsToAppend = newRows.filter(row => !isDuplicate(row, existingRows));
JSON.stringifyでの比較は雑に見えるが、会計明細のデータは日付・金額・摘要が全て一致すれば同一行とみなして問題ない。部分一致や曖昧マッチングを入れると逆に誤判定が増える。「全列一致なら重複」というルールで2週間運用して、誤判定は一度も起きていない。
UIの変更: チェックボックスからラジオボタンへ
元々のUIでは「CSVダウンロード」と「Sheets書き込み」を独立したチェックボックスで並べていた。しかしこの2つは排他的な操作で、両方同時に実行する意味がない。チェックボックスだとユーザーが両方ONにできてしまう。
ラジオボタンに変更して、出力先を1つだけ選ぶUIにした。小さな変更だが、操作の迷いが消えた。
ページ遷移の待機ロジックを書き直す
この拡張は複数の口座を順番に切り替えながら明細を取得する。口座切り替え時にページ遷移が発生し、DOMが再構築される。元の実装では固定1.5秒の setTimeout で待機していた。
固定待機の問題
最後の口座に切り替えた直後、フィルタ条件が適用される前のデータを拾ってしまう現象が出た。ネットワークが遅いタイミングで1.5秒では足りず、前の口座のデータがまだ表示されている状態でスクレイピングが走っていた。
修正: 条件ベースの待機
固定秒数の待機を捨てて、2つの条件が揃うまでポーリングする方式に変えた。
- テーブル要素が存在する(DOMの再構築完了)
- ドロップダウンに対象口座名が表示されている(フィルタ適用完了)
const waitForAccountReady = async (accountName, maxWait = 15000) => {
const start = Date.now();
while (Date.now() - start < maxWait) {
const table = document.querySelector('.data-table');
const dropdown = document.querySelector('.account-selector');
if (table && dropdown?.textContent.includes(accountName)) {
return true;
}
await new Promise(r => setTimeout(r, 300));
}
throw new Error(`Timeout: ${accountName} の読み込みが${maxWait}ms以内に完了しなかった`);
};
「テーブルが見える」だけでは不十分で、「正しい口座のデータが表示されている」まで確認する必要があった。固定秒数の待機は、遅いときに足りず、速いときに無駄に待つ。条件ベースなら両方解決する。
振り返り
半日で5つのエラーを踏んだ。OAuth2のクライアントIDタイプ、host_permissions漏れ、API URLの違い、全上書きの危険性、固定待機の不安定さ。どれも「動かしてみて初めてわかる」類のもので、ドキュメントを読むだけでは防げなかった。
1つだけ事前に防げたのがhost_permissionsで、これはCodexレビューのおかげだった。自分のコードを別の目で見てもらう仕組みは、たとえAIであっても機能する。
Chrome拡張 + Google Sheets APIの組み合わせは、認証の設定さえ乗り越えれば、スプレッドシートをデータベースのように使える。個人ツールとしてのコスパは高い。