Google Sheets APIをNodeで自動化:サービスアカウントで読み書き・定期更新・レポート生成
Node.jsとGoogle Sheets APIをサービスアカウント認証で自動化。行の読み書き、定期更新、レポート生成、CSV連携と、権限・共有でハマる定番を実例で。
「APIは200を返してる。なのにシートには1行も増えてない」
去年、問い合わせを自動でスプレッドシートに溜めるスクリプトを書いていた僕は、これで丸一日溶かしました。コードは正しい。トークンも取れてる。ステータスも成功。でもシートは空っぽ。
原因は、拍子抜けするほど単純でした。サービスアカウントにシートを共有し忘れていただけ。APIは「あなたが書ける場所には書きました」と正直に答えていて、ただその「書ける場所」が、僕の見ているシートではなかったんです。
Google Sheets APIの自動化でつまずく人の大半は、コードじゃなくてここで転びます。認証と共有設定。だから今日は、Node.jsでシートを読み書きする実コードと一緒に、その「権限の地雷」を先に踏んでおきます。
この記事の要点
- Google Sheets APIの自動化は、コードより先にサービスアカウントの共有設定で決まる。シートを共有しないと200が返っても書き込めない。
- 認証は個人OAuthよりサービスアカウントが楽。JSONキーを作り、その
client_emailをシートに編集者として共有するだけ。 - 読み書きは
values.getとvalues.appendの2つを押さえれば実務の8割は回る。コピペで動くコードを下に置いた。 - 定期更新はcronやGitHub Actionsで同じスクリプトを叩くだけ。レポートは「Rawに追記、集計は別シート」で壊れにくくなる。
- CSV取り込みやスクレイピング結果の流し込みは、Webスクレイピングの記事とデータ可視化の記事につながる。
まずサービスアカウントを用意する(ここが本番)
コードの前に、認証を片付けます。個人のGoogleアカウントでOAuthを通す方法もありますが、ブラウザでの同意画面、リフレッシュトークンの保存、トークン失効と、地味な手間が多い。サーバーやcronで黙々と動かすなら、人間の同意がいらないサービスアカウントが圧倒的に楽です。
サービスアカウントは、人間ではなくプログラム用のGoogleアカウントだと思ってください。専用のメールアドレス([email protected]みたいな形)を持っていて、そのアドレスにシートを共有すると、プログラムがそのアカウントとしてシートを触れます。
手順はこうです。
- Google Cloud Consoleでプロジェクトを作る(既存でもいい)。
- 「APIとサービス」→「ライブラリ」で Google Sheets API を有効化する。
- 「認証情報」→「サービスアカウントを作成」。名前は
sheets-botなど適当でいい。 - 作ったサービスアカウントの「キー」タブ→「鍵を追加」→ JSON を選んでダウンロード。これが
service-account.jsonになる。 - ダウンロードしたJSONを開き、
client_emailの値(前述のメールアドレス)をコピー。 - 対象のGoogleスプレッドシートを開き、右上の「共有」から、そのメールアドレスを「編集者」として追加する。
この6番が、冒頭で僕が忘れていたやつです。ここを飛ばすと、コードがどれだけ正しくてもThe caller does not have permissionか、もしくは何も起きません。先にやっておきましょう。
そしてダウンロードしたJSONキーは、絶対にGitに入れないでください。.gitignoreにservice-account.jsonを足しておきます。
echo "service-account.json" >> .gitignore
Node.jsからシートに書き込む(コピペで動く)
準備ができたら、いちばん使う「行を追記する」処理を作ります。googleapisを入れて、環境変数を2つ通すだけです。
npm install googleapis
export GOOGLE_APPLICATION_CREDENTIALS="$PWD/service-account.json"
export SHEET_ID="ここにスプレッドシートのIDを入れる"
SHEET_IDは、シートのURL https://docs.google.com/spreadsheets/d/【ここ】/edit の真ん中部分です。シート(タブ)にはRawという名前のタブを作り、1行目にcreatedAt,source,subject,amount,statusを入れておきます。
append-row.mjsを保存します。
import { google } from "googleapis";
const { GOOGLE_APPLICATION_CREDENTIALS, SHEET_ID } = process.env;
// 環境変数が無ければ静かに進まず、その場で止める
if (!GOOGLE_APPLICATION_CREDENTIALS) {
throw new Error("GOOGLE_APPLICATION_CREDENTIALS が未設定です");
}
if (!SHEET_ID) {
throw new Error("SHEET_ID が未設定です");
}
// サービスアカウントのJSONキーで認証する
const auth = new google.auth.GoogleAuth({
keyFile: GOOGLE_APPLICATION_CREDENTIALS,
scopes: ["https://www.googleapis.com/auth/spreadsheets"],
});
const sheets = google.sheets({ version: "v4", auth });
// コマンドライン引数を受け取る(例: node append-row.mjs web 相談 50000)
const source = process.argv[2] ?? "web";
const subject = process.argv[3] ?? "Claude Code 相談";
const amount = Number(process.argv[4] ?? 0);
// 金額が数字でなければ書き込まずに止める
if (!Number.isFinite(amount)) {
throw new Error(`金額が数値ではありません: ${process.argv[4]}`);
}
await sheets.spreadsheets.values.append({
spreadsheetId: SHEET_ID,
range: "Raw!A:E",
valueInputOption: "USER_ENTERED",
insertDataOption: "INSERT_ROWS",
requestBody: {
values: [[new Date().toISOString(), source, subject, amount, "new"]],
},
});
console.log("1行追記しました");
実行はこれだけです。
node append-row.mjs newsletter "スプレッドシート自動化の相談" 50000
シートのRawタブに1行増えていれば成功です。増えていないなら、まず共有設定を疑ってください。9割そこです。
覚えるポイントは3つだけ。rangeのRaw!A:Eは「RawタブのA〜E列」という意味。valueInputOptionをUSER_ENTEREDにすると、=SUM(...)のような文字列を入れたときちゃんと数式として解釈されます(生の文字列のまま入れたいならRAW)。insertDataOption: "INSERT_ROWS"は、既存データの下に行を差し込むおまじないです。
シートを読む:取得して集計する
書けたら、今度は読みます。読み取りはvalues.get一本。レンジを渡すと、二次元配列で返ってきます。
例として、Rawタブに溜めた問い合わせを読み、statusがwonの行だけ月別・チャネル別に集計してみます。read-and-summarize.mjsを保存します。
import { google } from "googleapis";
const { GOOGLE_APPLICATION_CREDENTIALS, SHEET_ID } = process.env;
if (!GOOGLE_APPLICATION_CREDENTIALS || !SHEET_ID) {
throw new Error("GOOGLE_APPLICATION_CREDENTIALS と SHEET_ID が必要です");
}
const auth = new google.auth.GoogleAuth({
keyFile: GOOGLE_APPLICATION_CREDENTIALS,
// 読むだけなので readonly スコープに絞る
scopes: ["https://www.googleapis.com/auth/spreadsheets.readonly"],
});
const sheets = google.sheets({ version: "v4", auth });
const res = await sheets.spreadsheets.values.get({
spreadsheetId: SHEET_ID,
range: "Raw!A:E",
});
const rows = res.data.values ?? [];
if (rows.length <= 1) {
console.log("データ行がありません");
process.exit(0);
}
// 1行目はヘッダーなので、列名→添字の対応を作る
const header = rows[0];
const idx = (name) => header.indexOf(name);
const col = {
createdAt: idx("createdAt"),
source: idx("source"),
amount: idx("amount"),
status: idx("status"),
};
const summary = new Map();
for (const row of rows.slice(1)) {
if (row[col.status] !== "won") continue;
const amount = Number(row[col.amount]);
if (!Number.isFinite(amount)) continue; // 壊れた金額は黙って数えない
const month = String(row[col.createdAt]).slice(0, 7); // 2026-06
const source = row[col.source] || "unknown";
const key = `${month}__${source}`;
const acc = summary.get(key) ?? { month, source, deals: 0, revenue: 0 };
acc.deals += 1;
acc.revenue += amount;
summary.set(key, acc);
}
for (const r of [...summary.values()].sort((a, b) => a.month.localeCompare(b.month))) {
console.log(`${r.month} / ${r.source}: ${r.deals}件 / ${r.revenue.toLocaleString()}円`);
}
実行します。
node read-and-summarize.mjs
ここで地味に大事なのが、列名から添字を引く書き方です。row[3]のように番号でベタ書きすると、誰かがシートに1列足した瞬間に集計が全部ズレます。header.indexOf("amount")で引いておけば、列順が変わっても壊れません。スプレッドシートは人が触る場所なので、列はいつか必ず動く、という前提で書いておくと夜中に泣かずに済みます。
定期更新とレポート生成
「行を読む・書く」ができたら、定期実行はもう半分終わっています。同じスクリプトを時間で叩くだけだからです。
サーバーがあるならcronで十分です。
# 毎朝7時に集計スクリプトを回す(crontab -e に貼る)
0 7 * * * cd /home/me/sheets-bot && GOOGLE_APPLICATION_CREDENTIALS=/home/me/sheets-bot/service-account.json SHEET_ID=xxxx /usr/bin/node read-and-summarize.mjs >> log.txt 2>&1
サーバーを持ちたくないなら、GitHub Actionsで定期実行できます。JSONキーはリポジトリに置かず、Secretsに入れてから書き出すのがコツです。
name: daily-sheet-report
on:
schedule:
- cron: "0 22 * * *" # UTC 22:00 = JST 7:00
workflow_dispatch: {}
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm ci
# Secrets に入れた JSON キーをファイルに書き出す
- run: echo '${{ secrets.GCP_SA_JSON }}' > service-account.json
- run: node read-and-summarize.mjs
env:
GOOGLE_APPLICATION_CREDENTIALS: service-account.json
SHEET_ID: ${{ secrets.SHEET_ID }}
レポートの作り方には、ひとつだけ強くおすすめしたい流儀があります。APIで書き込むのはRawかClean、人が読むReportは別シートで関数やピボットを使う、という分け方です。
| シート | 役割 | 誰が書く |
|---|---|---|
| Raw | 生データを溜めるだけ | APIが追記 |
| Clean | 型をそろえた中間データ | APIかスクリプト |
| Report | 人が読む月次集計やグラフ | 関数・ピボット・別スクリプト |
なぜ分けるか。人が見やすいReportシートは、結合セル、色、メモ、列の並び替えが入りがちで、機械が安定して書ける形と相性が最悪だからです。APIで触る場所を「ただ追記するだけのRaw」に限定しておくと、表示を人間が好きにいじっても、自動処理は壊れません。集計結果を別スクリプトでReportタブに書きたいなら、values.updateで範囲をまるごと上書きするのが安全です。
CSVと外部データをシートに流し込む
実務だと、決済CSV、広告CSV、スクレイピング結果みたいな外部データをシートに入れたい場面が多いはず。基本は「CSVを配列に直す → values.appendに渡す」だけです。
import { google } from "googleapis";
import { readFile } from "node:fs/promises";
const { GOOGLE_APPLICATION_CREDENTIALS, SHEET_ID } = process.env;
const auth = new google.auth.GoogleAuth({
keyFile: GOOGLE_APPLICATION_CREDENTIALS,
scopes: ["https://www.googleapis.com/auth/spreadsheets"],
});
const sheets = google.sheets({ version: "v4", auth });
// 簡易CSVパース(カンマを含むセルがある場合は専用パーサを使う)
const text = await readFile(process.argv[2] ?? "data/sales.csv", "utf8");
const rows = text.trim().split(/\r?\n/).map((line) => line.split(","));
// 1行目(ヘッダー)を捨てて中身だけ追記する
const body = rows.slice(1);
if (body.length === 0) {
throw new Error("追記する行がありません");
}
await sheets.spreadsheets.values.append({
spreadsheetId: SHEET_ID,
range: "Raw!A:Z",
valueInputOption: "USER_ENTERED",
insertDataOption: "INSERT_ROWS",
requestBody: { values: body },
});
console.log(`${body.length}行を流し込みました`);
注意点はふたつ。1回のappendに万単位の行を渡すと重くなったり制限に当たったりするので、大量データは500〜1000行ずつ分割して投げます。それと、上の簡易パースはセル内にカンマや改行があると壊れます。本気のCSVなら、,の手書き分割ではなくちゃんとしたパーサを使ってください。
スクレイピングで集めた表をそのまま流し込むなら、取得側の作り方はNode.jsでWebスクレイピングする記事に分けて書きました。シートに溜めた数字をグラフにする段はデータ可視化で先に決める3つのことが続きになります。
よくある質問
Q. APIは成功と言うのに、シートに何も書き込まれません。
ほぼ共有設定の漏れです。サービスアカウントのJSON内client_emailの値を、対象シートに「編集者」で共有しているか確認してください。The caller does not have permissionが出るのも同じ原因です。
Q. サービスアカウントとOAuth、どっちを使うべき? サーバーやcron、CI/CDで無人で動かすならサービスアカウント。エンドユーザー自身のシートを本人の権限で触る必要があるならOAuth、という分け方が無難です。社内の集計や自動化なら、まずサービスアカウントで十分です。
Q. Quota exceeded が出ます。
Google Sheets APIには「1分あたりのリクエスト数」上限があります。ループで1行ずつappendせず、複数行をまとめて1回で送るのが基本。それでも当たるなら、リトライ間隔を空けるか処理をバッチ化します。最新の上限はGoogle Sheets APIの公式ドキュメントで確認できます。
Q. ExcelやCSVではダメ? 共同編集や、誰でもブラウザで見られる手軽さが要るならGoogle Sheetsが向きます。逆に厳密な集計やバックアップが主目的なら、CSVでローカル完結させたほうが速いし壊れにくいです。データの正本(マスター)はDBに置き、Sheetsは確認・共有用に寄せるのが安全です。
Q. Claude Codeにこの自動化を作らせるとき、何を渡せばいい? 入力(どのCSVやレンジ)、出力(どのタブのどの列)、触ってよいファイル、検証コマンド、そして「認証情報を読まない・出力しない・コミットしない」。この5つを依頼文に明記すると、暴走と事故がほぼ消えます。
実際に試した結果
僕の運用でいちばん効いたのは、賢いコードでも便利なライブラリでもなく、最初にRaw・Clean・Reportの線を引いたことでした。
「APIで触るのはRawだけ。集計は別シートの関数。グラフはさらにその先」と決めてから、自動処理が壊れる回数が激減しました。人間がReportの見た目を勝手にいじっても、Rawはただ追記されるだけなので無傷なんです。
逆に、最初の頃にやらかしたのは全部この記事に書いたとおり。共有設定の漏れで半日溶かし、列を番号でベタ書きして集計をズラし、1行ずつappendしてQuota exceededを食らいました。どれも、コードの賢さとは無関係のところで転んでいます。
Google Sheets APIの自動化は、結局「認証と共有を先に片付け、APIが触る場所を狭く限定する」ゲームです。そこさえ押さえれば、読み書きそのものはgetとappendの2行で終わります。まずは上のコードでRawに1行追記するところから始めてみてください。
依頼文や検証コマンドの型を手元に置きたいならClaudeCodeLabの教材・テンプレートに一式まとめてあります。チームで売上・問い合わせ・請求のシートを扱うなら、権限と承認の線引きから一緒に設計するClaude Code研修・導入相談もどうぞ。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
制作会社がClaude Codeに触らせる前に決める権限チェックリスト
クライアントサイトを壊さずにAI編集を使うための、制作会社向け権限と確認の型です。
SaaSサポートのバグ報告をClaude Codeで再現手順に変える実務フロー
問い合わせ文をそのまま開発へ投げず、再現手順、証拠、次の一手に整えるサポート向け手順です。
Obsidianの古いメモをClaude Codeの指示書に変える10分ルーチン
Obsidianに溜めたメモが毎回ゴミになる人へ。事実・決定・未確認に仕分けして、Claude Codeがそのまま動ける指示書に変える朝の10分の型を紹介します。