## この記事で解決できること
「GASで作ったスプレッドシートのデータを、外部のNode.jsスクリプトやPlaywrightから読み書きしたい」——そんな場面に遭遇したことはないでしょうか。
Google Apps Scriptの `doGet` / `doPost` を使えば、**GASをAPIサーバーとして公開**し、外部システムと双方向にデータ連携できます。本記事では、実際に稼働しているフリーランス案件管理システムをベースに、以下のパターンを解説します。
- トークン認証で不正アクセスを防ぐ
- 関数ホワイトリストで実行可能な処理を制限する
- スプレッドシートのデータをJSON形式で返す
- POSTでバッチデータを投入する
## 全体構成
外部システム (Node.js / Playwright)
│
│ GET: シートデータ取得 / 関数実行
│ POST: バッチデータ投入
▼
GAS Web App (doGet / doPost)
│ トークン検証 → ルーティング → 処理
▼
Google Spreadsheet
## 1. GAS側: doGet / doPost の実装
### トークン認証 + ルーティング
```javascript
// ── 設定 ──────────────────────────────────────────────────
// Script Properties に VIEWER_TOKEN を登録しておく
function getToken_() {
return PropertiesService.getScriptProperties().getProperty('VIEWER_TOKEN');
}
// 実行を許可する関数のホワイトリスト
var ALLOWED_FUNCTIONS = [
'collectLeads',
'buildAiReviewPrompt',
'applyAiResponses',
'importLeadsFromNotes'
];
// ── doGet: データ取得 & 関数実行 ──────────────────────────
function doGet(e) {
var token = e.parameter.token;
// トークン認証
if (token !== getToken_()) {
return ContentService
.createTextOutput(JSON.stringify({ error: 'unauthorized' }))
.setMimeType(ContentService.MimeType.JSON);
}
// アクション分岐
var action = e.parameter.action;
// 関数実行モード
if (action === 'run') {
var fn = e.parameter.fn;
if (ALLOWED_FUNCTIONS.indexOf(fn) === -1) {
return jsonResponse_({ error: 'function not allowed: ' + fn });
}
// グローバルスコープから関数を取得して実行
var result = this[fn]();
return jsonResponse_({ ok: true, result: result });
}
// シートデータ返却モード
var sheetName = e.parameter.sheet || 'leads';
return jsonResponse_(getSheetData_(sheetName));
}
// ── doPost: バッチデータ投入 ─────────────────────────────
function doPost(e) {
var body = JSON.parse(e.postData.contents);
// トークン認証
if (body.token !== getToken_()) {
return jsonResponse_({ error: 'unauthorized' });
}
var action = body.action;
// バッチインポート
if (action === 'importLeads') {
var result = importLeadsBatch_(body.leads);
return jsonResponse_(result);
}
// 個別レコード更新
if (action === 'reviewLead') {
updateLeadReview_(body.row, body);
return jsonResponse_({ ok: true });
}
return jsonResponse_({ error: 'unknown action: ' + action });
}
ヘルパー関数
// JSON レスポンスを生成する共通関数
function jsonResponse_(data) {
return ContentService
.createTextOutput(JSON.stringify(data))
.setMimeType(ContentService.MimeType.JSON);
}
// スプレッドシートのデータをオブジェクト配列として返す
function getSheetData_(sheetName) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName(sheetName);
if (!sheet) {
return { error: 'sheet not found: ' + sheetName };
}
var data = sheet.getDataRange().getValues();
var headers = data[0]; // 1行目をヘッダーとして扱う
var rows = [];
for (var i = 1; i < data.length; i++) {
var row = {};
for (var j = 0; j < headers.length; j++) {
row[headers[j]] = data[i][j];
}
rows.push(row);
}
var result = {};
result[sheetName] = { headers: headers, rows: rows };
return result;
}
// バッチインポート(重複チェック付き)
function importLeadsBatch_(leads) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Leads');
var existing = sheet.getDataRange().getValues();
// 既存URLのセットを作成(重複排除用)
var existingUrls = {};
for (var i = 1; i < existing.length; i++) {
existingUrls[existing[i][1]] = true; // B列 = URL
}
var added = 0, skipped = 0;
for (var k = 0; k < leads.length; k++) {
var lead = leads[k];
if (existingUrls[lead.url]) {
skipped++;
continue;
}
sheet.appendRow([
new Date(), // 検出日時
lead.url, // URL
lead.title, // タイトル
lead.source, // ソース (CrowdWorks / Lancers)
lead.budget, // 予算
'Pending' // ステータス
]);
existingUrls[lead.url] = true;
added++;
}
return { ok: true, added: added, skipped: skipped };
}
2. Node.js側: GETでデータ取得
const axios = require('axios');
require('dotenv').config();
const GAS_URL = process.env.GAS_WEB_APP_URL;
const GAS_TOKEN = process.env.GAS_VIEWER_TOKEN;
// シートデータをJSON取得
async function fetchLeads() {
const res = await axios.get(
`${GAS_URL}?token=${GAS_TOKEN}&sheet=leads`,
{ maxRedirects: 5, timeout: 30000 }
);
const rows = res.data.leads.rows;
console.log(`取得件数: ${rows.length}`);
return rows;
}
// GAS関数をリモート実行
async function runGasFunction(fnName) {
const res = await axios.get(
`${GAS_URL}?token=${GAS_TOKEN}&action=run&fn=${fnName}`,
{ maxRedirects: 5, timeout: 60000 }
);
return res.data;
}
3. Node.js側: POSTでバッチ投入
Playwrightでスクレイピングした案件データをGASに一括投入するパターンです。
// スクレイピング結果をGASへバッチ送信
async function postLeadsToGas(leads) {
if (leads.length === 0) {
console.log('送信する案件なし');
return;
}
const response = await axios.post(
GAS_URL,
{
token: GAS_TOKEN,
action: 'importLeads',
leads: leads // [{ url, title, source, budget }, ...]
},
{
headers: { 'Content-Type': 'application/json' },
timeout: 45000,
maxRedirects: 5
}
);
console.log(`追加: ${response.data.added}件, 重複スキップ: ${response.data.skipped}件`);
return response.data;
}
4. デプロイ手順と注意点
Web Appとしてデプロイ
- GASエディタ → デプロイ → 新しいデプロイ
- 種類: ウェブアプリ
- 実行ユーザー: 自分
- アクセス: 全員(トークンで認証するため)
.env の設定
GAS_WEB_APP_URL=https://script.google.com/macros/s/DEPLOYMENT_ID/exec
GAS_VIEWER_TOKEN=your-secret-token-here
気をつけるポイント
| 項目 | 対策 |
|---|---|
| GASの実行時間制限(6分) | バッチ投入は1回あたりの件数を制限する |
| URLパラメータの文字数制限 | 長いデータはGETではなくPOSTを使う |
| GASの自動リダイレクト | axios等では maxRedirects: 5 を指定 |
| 同時実行によるロック |
LockService で排他制御する |
| トークンの管理 | Script Properties に格納し、コードに直書きしない |
LockServiceによる排他制御
function importLeadsBatch_(leads) {
// 同時書き込みを防ぐ
var lock = LockService.getScriptLock();
lock.waitLock(30000); // 最大30秒待つ
try {
// ... 書き込み処理 ...
} finally {
lock.releaseLock();
}
}
このパターンが活きるユースケース
- スクレイピング → スプレッドシート管理: Playwrightで取得したデータをGASに投入し、スプレッドシートで一覧管理
- AI評価の自動化: 外部でClaude等のLLMを呼び出し、評価結果をGASにPOSTで書き戻す
- Slack通知との連携: GAS側でSlack Webhook通知を仕込み、POSTのたびに自動通知
GASの「無料で常時稼働するAPIサーバー」という特性は、個人開発や小規模自動化で非常に強力です。Spreadsheet APIを直接叩くよりもロジックを集約でき、認証やバリデーションも一箇所で管理できます。
より詳しい実装例(3つのクラウドソーシングプラットフォーム対応・AI評価・提案文自動生成まで含めた実践的な構成)は、以下の記事で解説しています。
Qiita記事の本文を出力しました。実際のコードベース(`auto-review.js`、`crowdworks-scraper.js`)の実装パターンをベースに、トークン認証・ホワイトリスト・GET/POSTの使い分け・バッチ投入・排他制御まで網羅しています。約2800文字です。
---
## 関連リソース
- **[GitHub] GAS Automation Toolkit**: https://github.com/denden56-cloud/gas-automation-toolkit
- **[note] 全コード公開記事**: https://note.com/large_yarrow1156/n/nbbeaa3e6e1c8