GASドリル(実装演習)
第10〜12章のApps Scriptを「実際に書いて」身につける実装ドリル。値の読み書き・getValues/setValues一括処理・トリガー・差込メール・外部API・全角半角正規化・PropertiesService・例外処理・バッチ分割まで、難易度別の実装課題で鍛えます。
読むだけでは Apps Script は身につきません。 この章は、第10〜12章で学んだ GAS を 自分の手で書いて 確かめる実装ドリルです。各課題には、書くための場面と要件があり、自分で書いてから模範解答と見比べられます。ゴールは「型を見ずに自力で書ける」状態です。
基礎 — 値の読み書き
GAS の入口は SpreadsheetApp。「ブック → シート → 範囲 → 値」の階層をコードでたどります。
アクティブなシートの A1 にデータが入っています。
A1 の値をログに出力し、B1 に「確認済」と書き込む関数 check を書いてください。ここに Apps Script を書いてみましょう(実行は GAS エディタで)
function check() {
const sheet = SpreadsheetApp.getActiveSheet();
const value = sheet.getRange("A1").getValue(); // 1セルは getValue
Logger.log(value); // 実行ログに出力
sheet.getRange("B1").setValue("確認済"); // 1セルは setValue
}ポイント:getActiveSheet() で今のシートを取り、getRange("A1") で範囲(セル)を指定します。1セルの読み書きは getValue / setValue(複数なら getValues / setValues、後述)。Logger.log の出力は「実行ログ」で確認できます。
「ログ」という名前のシートに、行を追記していきます。
[日時, メッセージ] の1行を追加する関数 addLog(dateStr, msg) を書いてください。ここに Apps Script を書いてみましょう(実行は GAS エディタで)
function addLog(dateStr, msg) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("ログ");
sheet.appendRow([dateStr, msg]); // 配列を渡すと最終行の下に1行追加
}ポイント:名前でシートを取るのは getSheetByName("ログ")。appendRow(配列) は「最終行の次」に1行を追記する手軽なメソッドで、ログ蓄積に便利です。
一括処理 — getValues / setValues
GAS高速化の核心は「シートへのアクセス回数を減らす」こと。1セルずつではなく、まとめて読み、配列で計算し、まとめて書く のが鉄則です。
A列=単価、B列=数量(2行目以降)。C列に金額を入れたい。
C列へ一括で書き込む関数 fillAmount を書いてください(1セルずつのループは避ける)。ここに Apps Script を書いてみましょう(実行は GAS エディタで)
function fillAmount() {
const sheet = SpreadsheetApp.getActiveSheet();
const last = sheet.getLastRow();
if (last < 2) return; // データなし
// A2:B(last) を一度に取得(2次元配列)
const data = sheet.getRange(2, 1, last - 1, 2).getValues();
// メモリ上で計算(C列ぶんの2次元配列を作る)
const out = data.map(function (row) {
return [row[0] * row[1]];
});
// C2 から一度に書き戻す
sheet.getRange(2, 3, out.length, 1).setValues(out);
}ポイント:getRange(行, 列, 行数, 列数) の数値指定で範囲をまとめて取り、getValues() で2次元配列に。計算は配列に対して行い、setValues() で一括書き込み。ループの中で getValue/setValue を呼ぶと、行数ぶんシートへ往復して激遅になり、実行時間の上限にも引っかかります。
A列=宛先、B列=件名、C列=本文(2行目以降)。
sendAll を書いてください。宛先が空の行はスキップします。ここに Apps Script を書いてみましょう(実行は GAS エディタで)
function sendAll() {
const sheet = SpreadsheetApp.getActiveSheet();
const last = sheet.getLastRow();
if (last < 2) return;
const rows = sheet.getRange(2, 1, last - 1, 3).getValues();
rows.forEach(function (row) {
const to = row[0], subject = row[1], body = row[2];
if (to) { // 宛先が空ならスキップ
MailApp.sendEmail(to, subject, body);
}
});
}ポイント:getValues で一括取得してから forEach で送信します。MailApp.sendEmail(宛先, 件名, 本文) が基本形。1日の送信通数には上限があるため、大量送信の前に 公式のクォータ を確認してください。
トリガー — 自動で動かす
手動実行から一歩進み、「編集したら」「毎朝」「フォーム送信時に」自動で動かします。単純トリガーとインストーラブルトリガーの違いを意識します。
誰かがセルを編集したら、その行の N 列(14列目)に編集時刻を自動で記録したい。
onEdit(e) を書いてください。編集された行の14列目に現在時刻を入れます。ここに Apps Script を書いてみましょう(実行は GAS エディタで)
function onEdit(e) {
const range = e.range; // 編集されたセル範囲
const sheet = range.getSheet();
// 編集された行の14列目(N列)に現在時刻を記入
sheet.getRange(range.getRow(), 14).setValue(new Date());
}ポイント:onEdit という名前の関数は 単純トリガー として、編集時に自動で実行されます(イベント引数 e に編集位置などが入る)。単純トリガーは手軽な反面、メール送信や外部API呼び出しなど権限が要る操作はできません。それらが必要なら、ScriptApp.newTrigger(...).forSpreadsheet(...).onEdit().create() で インストーラブルトリガー を作ります。
利用者が手動で関数を実行できるよう、メニューを用意したい。
fillAmount が走るようにする onOpen を書いてください。ここに Apps Script を書いてみましょう(実行は GAS エディタで)
function onOpen() {
SpreadsheetApp.getUi()
.createMenu("ツール")
.addItem("金額を計算", "fillAmount") // 表示名, 実行する関数名(文字列)
.addToUi();
}ポイント:onOpen も単純トリガー。getUi().createMenu(...).addItem(表示名, 関数名).addToUi() の流れで独自メニューを作れます。addItem の第2引数は 関数名の文字列 である点に注意(fillAmount() と書くと誤り)。
「売上」シートの C 列(金額)の合計を、毎朝メールで受け取りたい。
dailyReport と、(2) それを毎日9時台に実行するトリガーを登録する setup の2つを書いてください。ここに Apps Script を書いてみましょう(実行は GAS エディタで)
function dailyReport() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("売上");
const total = sheet.getRange("C2:C").getValues()
.reduce(function (sum, row) { return sum + (Number(row[0]) || 0); }, 0);
MailApp.sendEmail("[email protected]", "日次売上", "現時点の合計: " + total);
}
function setup() {
// この setup を一度だけ手動実行すると、以後は毎日9時台に dailyReport が自動実行される
ScriptApp.newTrigger("dailyReport")
.timeBased()
.atHour(9)
.everyDays(1)
.create();
}ポイント:定期実行は インストーラブルトリガー(ScriptApp.newTrigger)。timeBased().atHour(9).everyDays(1) で毎日9時台に走ります。atHour は「その時間台」の意味で、分単位の厳密指定ではありません。トリガーは setup を一度実行して登録します(毎回作るとトリガーが増殖するので注意)。
Google フォームの回答がスプレッドシートに溜まります。回答列は[タイムスタンプ, メール, 名前]の順。
onFormSubmit(e) を書いてください。ここに Apps Script を書いてみましょう(実行は GAS エディタで)
function onFormSubmit(e) {
const values = e.values; // [タイムスタンプ, メール, 名前]
const email = values[1];
const name = values[2];
MailApp.sendEmail(email, "受付完了", name + " 様\n回答を受け付けました。");
}ポイント:e.values には1行ぶんの回答が 列の並び順 で配列として入ります。メール送信を伴うため、これは インストーラブルトリガー として登録します(単純トリガーの onFormSubmit では送信できません)。登録は、エディタ左の [トリガー]→[トリガーを追加] で、実行する関数に onFormSubmit・イベントの種類に 「フォーム送信時」 を選びます(コードからは ScriptApp.newTrigger("onFormSubmit").forSpreadsheet(SpreadsheetApp.getActive()).onFormSubmit().create())。
連携・正規化 — シートの外とつなぐ
外部APIから値を取り込んだり、関数では難しい変換をカスタム関数で解決します。
ある API が {"rate": 150.2} のような JSON を返します。
rate の値を A1 に書き込む関数 fetchRate を書いてください(URLは https://api.example.com/rate とする)。ここに Apps Script を書いてみましょう(実行は GAS エディタで)
function fetchRate() {
const res = UrlFetchApp.fetch("https://api.example.com/rate");
const json = JSON.parse(res.getContentText()); // 本文を文字列で取り出して JSON 化
SpreadsheetApp.getActiveSheet().getRange("A1").setValue(json.rate);
}ポイント:UrlFetchApp.fetch(URL) でHTTP取得、getContentText() で本文(文字列)を取り、JSON.parse でオブジェクトに。POSTやヘッダ付与は第2引数のオプションで指定します。外部送信を伴うので、単純トリガーからは呼べません。
「ABC123」と「ABC123」が混在し、VLOOKUP のキーが一致しません。標準関数だけでは一括変換できません。
=NORMALIZE(A2) や =NORMALIZE(A2:A100) で呼べる、NFKC 正規化のカスタム関数 NORMALIZE を書いてください(範囲にも対応)。ここに Apps Script を書いてみましょう(実行は GAS エディタで)
function NORMALIZE(value) {
if (Array.isArray(value)) { // 範囲(2次元配列)が渡された場合
return value.map(function (row) {
return row.map(function (cell) {
return cell === '' || cell === null ? '' : cell.toString().normalize('NFKC');
});
});
}
return value === '' || value === null ? '' : value.toString().normalize('NFKC'); // 単一セル
}ポイント:JavaScript の normalize('NFKC') が、全角英数字 ABC→ABC・半角カナ カナ→カナ・全角スペース→半角などを一括で標準形にそろえます。範囲対応にするには、引数が配列(範囲)のときだけ map で各セルを変換します。詳しい解説は 第11章 全角・半角をそろえる正規化。
安全運用 — 機密・エラー・大量データ
「動く」だけでなく「安全に動かし続ける」ためのコードです。
API キーをコードに直書きすると、共有時に漏れます。
saveKey と、(2) 保存したキーを使って API を呼ぶ useKey を書いてください。ここに Apps Script を書いてみましょう(実行は GAS エディタで)
function saveKey() {
// 一度だけ実行してキーを保存(コードには値を残さず、後で消してよい)
PropertiesService.getScriptProperties().setProperty("API_KEY", "ここに実際のキー");
}
function useKey() {
const key = PropertiesService.getScriptProperties().getProperty("API_KEY");
const res = UrlFetchApp.fetch("https://api.example.com/data", {
headers: { Authorization: "Bearer " + key }
});
Logger.log(res.getContentText());
}ポイント:PropertiesService.getScriptProperties() はスクリプトに紐づくキー値ストア。API キーやパスワードは コードに直書きせず ここに保存し、getProperty で取り出します。これで共有・公開してもコード中に機密が残りません。
自動実行の処理が失敗しても、誰も気づかないのは危険です。
riskyTask() を実行し、失敗したら管理者にエラー内容をメールする safeRun を書いてください。ここに Apps Script を書いてみましょう(実行は GAS エディタで)
function safeRun() {
try {
riskyTask();
} catch (err) {
MailApp.sendEmail("[email protected]", "処理エラー", String(err));
// 必要なら再スローして実行ログにも残す: throw err;
}
}ポイント:自動実行(トリガー)は画面に張り付いていないため、try/catch で失敗を捕まえ、通知や記録に回すのが定石です。String(err) でエラー内容を文字列化して本文に入れます。握りつぶすだけにせず、通知やログを必ず伴わせましょう。
数万行を1回のトリガーで処理すると、実行時間の上限で途中停止します。
processBatch を書いてください。ここに Apps Script を書いてみましょう(実行は GAS エディタで)
function processBatch() {
const props = PropertiesService.getScriptProperties();
const start = Number(props.getProperty("cursor") || 2); // 既定は2行目から
const sheet = SpreadsheetApp.getActiveSheet();
const last = sheet.getLastRow();
if (start > last) { props.deleteProperty("cursor"); return; } // 完了
const SIZE = 100;
const end = Math.min(start + SIZE - 1, last);
const rows = sheet.getRange(start, 1, end - start + 1, 1).getValues();
rows.forEach(function (row) {
// ここで1件ぶんの処理
});
if (end < last) {
props.setProperty("cursor", String(end + 1)); // 続きの位置を保存
} else {
props.deleteProperty("cursor"); // 最後まで終わった
}
}ポイント:実行時間の上限を超えそうな処理は、一定件数ずつに区切り、進捗(次に処理する行)を PropertiesService に保存します。時間主導トリガーで processBatch を定期実行すれば、毎回「続きから」少しずつ進められます。上限の詳細は 公式のクォータ を参照。
関数とGASのドリルを一巡したら、第10章 GAS入門・第11章 GAS実践パターン・第12章 連携と運用 で型を復習し、関数ドリル と合わせて「読む↔書く」を往復してください。用語は 用語集、一次情報は 参考資料 に。