この記事でやること
- DocumentApp の setAttributes で書式を一括適用
- findText / replaceText で検索・置換の実践と注意点
- スプレッドシート×テンプレで差し込み一斉メール(最小実装)
- 実務向けにログ/再送/プレビュー/スロットリングを整える
[TOC]
1. 前提と環境
-
ランタイム:Google Apps Script(V8)
-
スクリプト種別:
-
ドキュメントにバインド:DocumentApp.getActiveDocument() が使いやすい
-
スタンドアロン:DocumentApp.openById(DOC_ID) を推奨
-
サンプルは日本語ロケール(ja-JP)を例示
2. 超最小サンプル:段落を追加して書式を一括適用
/**
* ドキュメント末尾に段落を追加し、setAttributesで書式を一括適用
*/
function demoSetAttributes() {
const doc = DocumentApp.getActiveDocument(); // スタンドアロンなら openById()
const body = doc.getBody();
const now = new Date();
const today = now.toLocaleDateString('ja-JP'); // 例: 2025/9/23
const p = body.appendParagraph(`今日は ${today} の学習メモです`);
p.setAttributes({
[DocumentApp.Attribute.BOLD]: true,
[DocumentApp.Attribute.FOREGROUND_COLOR]: '#FF0000',
[DocumentApp.Attribute.ALIGNMENT]: DocumentApp.TextAlignment.CENTER,
[DocumentApp.Attribute.HEADING]: DocumentApp.ParagraphHeading.HEADING2
});
}
使いどころ & 注意
-
setAttributesは複数の装飾をまとめて適用できるのが強み -
段落系
(HEADING/ALIGNMENT)はParagraph、文字装飾は Text に -
文字列の一部だけ装飾するなら
Text#setAttributes(start, end, attrs)
3. 検索と置換:単発→複数一致→安全な置換
3.1 単発検索と全置換(最小)
function demoSearchAndReplace() {
const doc = DocumentApp.getActiveDocument();
const body = doc.getBody();
// 単発検索(最初の一致のみ)
const m = body.findText('書式設定');
Logger.log(m ? 'テキストが見つかりました' : 'テキストが見つかりませんでした');
// 全置換(正規表現として解釈される)
body.replaceText('書式設定', 'テキスト検索');
Logger.log('置換が完了しました');
}
3.2 複数一致を走査して部分装飾(太字+色)
function highlightAllTodo() {
const body = DocumentApp.getActiveDocument().getBody();
let range; // RangeElement
while (range = body.findText('TODO')) { // 逐次検索
const text = range.getElement().asText();
const s = range.getStartOffset();
const e = range.getEndOffsetInclusive();
text.setAttributes(s, e, {
[DocumentApp.Attribute.BOLD]: true,
[DocumentApp.Attribute.FOREGROUND_COLOR]: '#1E90FF'
});
}
}
3.3 安全な置換のための正規表現エスケープ
replaceText は正規表現として解釈されます。[](){}+*?.| などを含むと意図外ヒットの原因に。
差し込みは{{name}}のような分かりやすい記号を採用し、エスケープ関数を用意すると堅牢です。
/** 正規表現のメタ文字をエスケープ */
function escapeRegExp(str) {
return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function safeReplaceAll(body, placeholder, replacement) {
const pattern = escapeRegExp(placeholder); // 正規表現に安全化
body.replaceText(pattern, replacement); // すべて置換
}
4. 差し込み一斉メール(最小実装)
4.1 想定データ
- スプレッドシート
Recipients
A列: name / B列: email / C列: company(任意) / D列: sentAt(送信ログ)
- ドキュメントテンプレ(任意のDoc)
本文に{{name}}, {{company}}などのプレースホルダを配置
4.2 実装(Docs→テキスト→置換→送信)
※装飾が不要な最小構成。装飾を維持したい場合は §5 参照。
function mailMergeFromSheet_min() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Recipients');
const values = sh.getDataRange().getValues(); // 1行目はヘッダ想定
const TEMPLATE_DOC_ID = 'PUT_YOUR_DOC_ID_HERE';
const doc = DocumentApp.openById(TEMPLATE_DOC_ID);
const templateText = doc.getBody().getText(); // 純テキストで取得
for (let r = 1; r < values.length; r++) {
const [name, email, company] = values[r];
if (!email) continue;
const html = templateText
.replaceAll('{{name}}', name)
.replaceAll('{{company}}', company || '');
const subject = `【ご案内】${name} 様`;
MailApp.sendEmail({
to: email,
subject,
htmlBody: html
});
sh.getRange(r + 1, 4).setValue(new Date()); // D列: 送信時刻
Utilities.sleep(150); // スロットリング
}
}
5. 実務向け強化:ログ/再送/プレビュー/スロットリング
5.1 ログ+再送の型
-
status 列(E列)を追加:SENT / ERROR -
再送関数は
status !== 'SENT'の行のみ処理
function mailMergeFromSheet_pro() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Recipients');
const values = sh.getDataRange().getValues();
const TEMPLATE_DOC_ID = 'PUT_YOUR_DOC_ID_HERE';
const doc = DocumentApp.openById(TEMPLATE_DOC_ID);
const templateText = doc.getBody().getText();
const DEBUG_TO = ''; // プレビュー時は自分のメールを設定、空なら本番送信
for (let r = 1; r < values.length; r++) {
const [name, email, company, sentAt, status] = values[r];
if (status === 'SENT') continue; // 送信済みはスキップ
if (!email) {
sh.getRange(r + 1, 5).setValue('ERROR: no email'); // E列
continue;
}
try {
const html = templateText
.replaceAll('{{name}}', name)
.replaceAll('{{company}}', company || '');
const subject = `【ご案内】${name} 様`;
MailApp.sendEmail({
to: DEBUG_TO || email, // プレビュー時は自分宛へ
subject,
htmlBody: html
});
sh.getRange(r + 1, 4).setValue(new Date()); // D: sentAt
sh.getRange(r + 1, 5).setValue('SENT'); // E: status
Utilities.sleep(150);
} catch (err) {
sh.getRange(r + 1, 5).setValue(`ERROR: ${err.message}`);
Logger.log(JSON.stringify({
where: 'mailMergeFromSheet_pro',
row: r + 1,
message: err.message,
stack: err.stack
}));
}
}
}
5.2 正規表現ベースの安全置換(ユーティリティ)
function renderTemplateText(template, data) {
// data 例: { name: '山田', company: 'ABC' }
return Object.keys(data).reduce((acc, key) => {
const ph = `{{${key}}}`;
const pattern = new RegExp(escapeRegExp(ph), 'g');
return acc.replace(pattern, data[key] ?? '');
}, template);
}
5.3 クォータ対策と運用のポイント
-
送信クォータに注意(件数の多い配信は時間主導トリガーで分割)
-
プレビュー→本番の2段階運用(DEBUG_TO を空にしたら本番)
-
失敗時に止めない設計(try/catchでログを残して続行)
6. よくある落とし穴
-
replaceTextは正規表現:[](){}+*?.|などはエスケープ必須。プレースホルダは{{name}}のように設計 -
要素型の混同:段落系は
Paragraph、文字装飾はTextに適用 -
複数ヒットの扱い:
findTextは逐次検索。複数はwhile (range = ...)で走査 -
getActiveDocument()の文脈:スタンドアロンならopenById()を使う -
装飾つきHTMLを送りたい:
Docs→HTMLの抽出はコツが要るので、まずはHtmlServiceのテンプレ路線が楽
7. まとめと次の一歩(PDF化・テンプレ管理)
-
基礎3点(書式一括・検索・置換)だけで、差し込みメールは実現できる
-
実務では ログ/再送/プレビュー/スロットリング を添えて“壊れない仕組み”へ
-
次は Docsを複製→置換→PDF化→添付送信(請求書・参加証)に広げる
参考:PDF添付の最小例
function sendPdfAttachment() {
const TEMPLATE_DOC_ID = 'PUT_YOUR_DOC_ID_HERE';
const DEST_FOLDER_ID = 'PUT_YOUR_FOLDER_ID_HERE';
const folder = DriveApp.getFolderById(DEST_FOLDER_ID);
const copy = DriveApp.getFileById(TEMPLATE_DOC_ID).makeCopy(`ご案内_${Date.now()}`, folder);
const doc = DocumentApp.openById(copy.getId());
const b = doc.getBody();
// シンプル置換
b.replaceText('{{name}}', '山田太郎');
b.replaceText('{{company}}', 'ABC株式会社');
doc.saveAndClose();
// PDF化して添付
const pdfBlob = DriveApp.getFileById(copy.getId()).getAs(MimeType.PDF).setName('ご案内.pdf');
MailApp.sendEmail({
to: 'sample@example.com',
subject: '資料送付',
htmlBody: '資料をお送りします。',
attachments: [pdfBlob]
});
}
付録:ユーティリティ(コピペ用)
/** 正規表現エスケープ */
function escapeRegExp(str) {
return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/** 段落のスタイル適用 */
function applyParagraphStyle(p, opts = {}) {
const attrs = {
[DocumentApp.Attribute.BOLD]: !!opts.bold,
[DocumentApp.Attribute.FOREGROUND_COLOR]: opts.color || null,
[DocumentApp.Attribute.ALIGNMENT]: opts.align || DocumentApp.TextAlignment.LEFT,
[DocumentApp.Attribute.HEADING]: opts.heading || DocumentApp.ParagraphHeading.NORMAL
};
p.setAttributes(attrs);
}
/** findText で複数一致に装飾 */
function decorateAll(pattern, textAttrs) {
const body = DocumentApp.getActiveDocument().getBody();
let range;
while (range = body.findText(pattern)) {
const t = range.getElement().asText();
const s = range.getStartOffset();
const e = range.getEndOffsetInclusive();
t.setAttributes(s, e, textAttrs);
}
}
付録:ユーティリティ
/** 正規表現エスケープ */
function escapeRegExp(str) {
return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/** 段落のスタイル適用 */
function applyParagraphStyle(p, opts = {}) {
const attrs = {
[DocumentApp.Attribute.BOLD]: !!opts.bold,
[DocumentApp.Attribute.FOREGROUND_COLOR]: opts.color || null,
[DocumentApp.Attribute.ALIGNMENT]: opts.align || DocumentApp.TextAlignment.LEFT,
[DocumentApp.Attribute.HEADING]: opts.heading || DocumentApp.ParagraphHeading.NORMAL
};
p.setAttributes(attrs);
}
/** findText で複数一致に装飾 */
function decorateAll(pattern, textAttrs) {
const body = DocumentApp.getActiveDocument().getBody();
let range;
while (range = body.findText(pattern)) {
const t = range.getElement().asText();
const s = range.getStartOffset();
const e = range.getEndOffsetInclusive();
t.setAttributes(s, e, textAttrs);
}
}
学習の背景や気づきはnoteにまとめました → note版はこちら