在做簡報自動化時,第一個繞不開的關卡,就是怎麼正確「打開那一份指定的簡報」。
SlidesApp.openById() 這個方法看起來只有短短幾個字,卻是整個流程的起點:只要把檔案的 ID 丟進去,你就能拿到 Presentation 物件,接著新增投影片、改文字、插圖片、套用範本,一路到輸出 PDF、寄發報告都行。
這篇文章用白話把重點講清楚:ID 到底在哪裡找、和 getActivePresentation() 差在哪、在不同情境(像是從試算表觸發或排程)怎麼穩定串接,還會示範幾段實戰程式碼,列出常見錯誤與容易踩的雷,幫你少走彎路,讓自動化從今天就能跑起來。希望本篇文章能夠幫助到需要的您。
目錄
{tocify} $title={目錄}
先講重點:openById() 到底在幹嘛?
SlidesApp.openById(id) 是 Google Apps Script 中,用來打開指定 ID 的簡報並取得 Presentation 物件的方法。拿到 Presentation 之後,你就能對投影片做各種操作(加文字、插圖、複製版型、替換佔位符、導出成 PDF…)。這是處理多份簡報、自動化報告與批次更新的起點方法。官方文件對這點寫得很直白,也提供了基本用法示例。
進一步說,openById() 回傳的是 Presentation 類別的實例;而每份簡報都有一個唯一的 presentationId。你也能從 Presentation.getId() 反查目前操作的簡報 ID,這在除錯或記錄用很方便。
什麼情境必用 openById()?
跨文件自動化:
從 Google 試算表或文件的 Apps Script 腳本,去開啟某份既有簡報並更新內容。
範本產出:
有一份「母版簡報」當範本,把多筆資料套入後產生個人化簡報(如每個業務一份、每個客戶一份)。
排程報表:
搭配觸發器(時間驅動觸發),每天或每週自動開啟、更新、輸出並寄送簡報。
一步一步:從「拿到 ID」到「成功操作簡報」
第 1 步:正確取得 presentationId
1. 在瀏覽器打開你的 Google 簡報,URL 大致長這樣:
https://docs.google.com/presentation/d/1A2b3C...XYZ/edit#slide=id.p
其中 /d/ 與 /edit 之間那長長一串,就是 presentationId。
2. 只需要那段 ID,不要把整個網址丟進去。
小提醒:把 ID 存成常數或寫在屬性服務(PropertiesService),以便重複使用與版本管理。
第 2 步:在 Apps Script 環境寫下最小可行碼
以下範例使用 Apps Script(JavaScript),因為 SlidesApp API 原生就是這個環境。
function openMyDeck() {
const PRESENTATION_ID = '你的-presentationId';
const pres = SlidesApp.openById(PRESENTATION_ID);
Logger.log(`Title: ${pres.getName()}, ID: ${pres.getId()}`);
}
第 3 步:確認授權與存取權限
第一次執行會跳授權流程。Slides 的 Apps Script 需要對簡報有讀寫權限,並授權對應範圍(Scopes)。官方 Presentation 類別頁面也提到相關授權範圍(如 https://www.googleapis.com/auth/presentations)。
如果簡報不在你帳號可存取的雲端硬碟範圍(或沒分享給你),就會 403/404。
第 4 步:讀取與修改內容(入門範例)
function demo_read_update() {
const ID = '你的-presentationId';
const pres = SlidesApp.openById(ID);
const slides = pres.getSlides();
// 讀取第一張投影片的所有形狀文字
const page = slides[0];
const shapes = page.getPageElements()
.filter(e => e.asShape && e.asShape().getText);
shapes.forEach((el, idx) => {
try {
const shape = el.asShape();
const textRange = shape.getText();
Logger.log(`S${idx}: ${textRange.asString()}`);
} catch (err) {
// 有些元素不是文字形狀,會丟錯,包 try/catch 比較穩
}
});
// 在第一張投影片新增一個文字方塊
page.insertShape(SlidesApp.ShapeType.TEXT_BOX, 60, 60, 400, 80)
.getText()
.setText('Hello from Apps Script');
}
第 5 步:套範本(替換佔位符)
function fillTemplateDeck() {
const ID = '你的-範本簡報ID';
const replacements = {
'{NAME}': '王小明',
'{DATE}': Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy/MM/dd'),
'{SCORE}': '95'
};
const pres = SlidesApp.openById(ID);
const slides = pres.getSlides();
slides.forEach(slide => {
slide.getPageElements().forEach(el => {
if (el.asShape && el.asShape().getText) {
const text = el.asShape().getText();
let s = text.asString();
let changed = false;
for (const [key, val] of Object.entries(replacements)) {
if (s.includes(key)) {
s = s.split(key).join(String(val));
changed = true;
}
}
if (changed) {
text.setText(s);
}
}
});
});
}
第 6 步:輸出或寄送
你可以將簡報轉成 PDF blob,再寄出。
(注意:SlidesApp 本身主要對投影片操作;寄信用 GmailApp 或 MailApp。)
function exportAndSend() {
const ID = '你的-presentationId';
const file = DriveApp.getFileById(ID);
const pdf = file.getAs(MimeType.PDF);
MailApp.sendEmail({
to: 'you@example.com',
subject: '本日報告',
body: '附件為自動產出的簡報 PDF。',
attachments: [pdf]
});
}
你可能會遇到的 10 大錯誤與踩雷
1. 把整個 URL 丟進 openById()
症狀:Invalid argument 或 File not found。
解法:只放 ID,那段在 /d/ 與 /edit 中間的字串。
2. 沒有權限(簡報沒分享給腳本執行者)
症狀:403/404。
解法:確認雲端硬碟分享設定,或用同組織帳號執行。openById() 不是超能力,打不開你無權限的檔案。
3. 在不對的容器或時機呼叫
案例:想在 Slides 的 onOpen() 事件裡再去 openById() 別的簡報,有時會遇到限制/不預期行為。社群討論多次提及此類情況不可行或不建議。建議把開別份簡報的邏輯放在一般函式或後台腳本,由使用者從選單點擊觸發。
4. 把 Advanced Slides API 和 SlidesApp 搞混
SlidesApp 是 Apps Script 內建服務;Advanced Slides API 是另一條路(需在服務中啟用、走 REST 配額)。你的需求只是「開啟並修改簡報」,多半 SlidesApp 就夠,不必額外啟用。若要走大量 API 呼叫與後端架構,才考慮 Advanced API 與其配額。官方配額文件明確列出 REST 請求頻率與超限回應(429,建議使用指數退避)。
5. 配額與節流(Quota/Rate Limit)
症狀:大量自動化、短時間改很多簡報,開始噴錯或變慢。
解法:理解 Apps Script/Slides API 的配額規則(如每分鐘/每使用者限制)。超出時採 exponential backoff(指數退避)重試,並分批處理。
6. 觸發器跑太久或超時
Apps Script 的觸發器有每日/執行時間配額,長流程請拆段或使用批次機制。
7. 佔位符替換沒生效
症狀:文字看起來一樣,但程式找不到。
解法:檢查是否是智慧引號或不可見字元,或是文字在群組/表格/圖形內。實務上用 asString() 檢查輸出,再對照實際畫面。
8. 誤判元素型別
getPageElements() 會回各種元素(圖片、形狀、表格…),不是每個都有 getText()。加上 if (el.asShape && el.asShape().getText) 或 instanceof 的保護最穩。
9. 多國語/字體造成版面跑版
自動化插入大量文字時,字體 fallback 或行高差異會讓版面抖動。
解法:在範本中鎖定字體與行距;必要時用固定寬高的文字方塊並設定 TextAutoResize。
10. 團隊協作下的 ID 管理
切環境(測試/正式)常忘了換 ID。
解法:用 腳本屬性(Script Properties) 管理不同環境的 ID,或從設定表(Google Sheet)讀取。
進階技巧:讓 openById() 發揮更大價值
1. 從資料驅動模板,批次產生 N 份簡報
把客戶清單或 KPI 放在 Google Sheet,逐行讀入,對每行跑一次填充流程,最後輸出/寄送。
function batchGenerate() {
const TEMPLATE_ID = '範本ID';
const sheet = SpreadsheetApp.getActive().getSheetByName('DATA');
const rows = sheet.getDataRange().getValues();
const headers = rows.shift();
rows.forEach((row, idx) => {
const data = Object.fromEntries(headers.map((h, i) => [ `{${h}}`, row[i] ]));
const pres = SlidesApp.openById(TEMPLATE_ID);
pres.getSlides().forEach(slide => {
slide.getPageElements().forEach(el => {
if (el.asShape && el.asShape().getText) {
const t = el.asShape().getText();
let s = t.asString();
let changed = false;
for (const k in data) {
if (s.includes(k)) { s = s.split(k).join(String(data[k])); changed = true; }
}
if (changed) t.setText(s);
}
});
});
// 另存副本:建議用 Drive API/Files.copy 或建立新簡報再貼內容(視需求)
// 簡易示例:導出 PDF 命名
const pdf = DriveApp.getFileById(TEMPLATE_ID).getAs(MimeType.PDF);
const name = `報表_${idx+1}.pdf`;
DriveApp.getFolderById('你的輸出資料夾ID').createFile(pdf).setName(name);
});
}
若你走的是 Advanced Slides API / REST,請同時留意該 API 的每分鐘讀寫配額與 429 錯誤的退避策略。
2. 與其他服務協同
Gmail:自動寄送產出 PDF。
Drive:控管檔案歸檔、命名規則、權限調整。
Calendar:排程提醒發送日報/週報。
Docs:把投影片備註或內容彙整成文件(社群實務常見)。
3. 穩定性與速度
批次寫入:盡量把讀取與字串處理在記憶體完成,最後一次性 setText()。
減少 DOM 走訪:先定位需要處理的頁面或以命名佔位元素作為錨點。
錯誤處理:用 try/catch 包住高風險段落;衝配額時加上簡單退避(如 500ms→1s→2s)。
風險控管與配額(Quota)觀念
雖然 SlidesApp.openById() 本身呼叫不重,但大量腳本操作仍會撞到 Apps Script 或 Slides API 的配額與速率限制。
Apps Script 服務配額(包含建立檔案、觸發器總執行時間等)官方有統一頁面說明。企業帳與個人帳配額不同,且多為每日重置。
Slides REST API(若你採 REST 方式)則有每分鐘 per project / per user 的讀寫限制;若超限會回 HTTP 429 並建議實作 exponential backoff。
實務建議:長批次任務切段執行、加入延遲與退避、避開高峰時段。
問題集
Q1:可以從 Google 試算表的腳本去改 Slides 嗎?
可以。SlidesApp.openById() 在任何 Apps Script 環境都能用,只要帳號有權限。很多人都是在試算表按鈕一鍵產簡報。
Q2:openById() 和 getActivePresentation() 差在哪?
getActivePresentation():在簡報容器裡使用,拿到目前這份正在開的簡報。
openById():指定 ID 開別份檔,適合跨文件自動化或範本流程。
Q3:為什麼我在 onOpen() 想開另一份簡報會怪怪的?
容器的生命週期與 UI 限制導致,有社群回覆指出無法如願或不建議。把「開別份簡報」放到一般函式並由自訂選單點擊觸發,較穩。
Q4:我需要啟用 Advanced Slides API 嗎?
多數「在同帳號下更新簡報內容」的需求,用 SlidesApp 內建服務就夠。若你要 REST 方式、大量並行或與後端雲端專案整合,才考慮 Advanced API 與其速率/配額。
實用小片段
用正規表達式做全文替換(支援多個鍵)
function replaceAllTexts(pres, map) {
const slides = pres.getSlides();
const keys = Object.keys(map).map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
if (keys.length === 0) return;
const regex = new RegExp(keys.join('|'), 'g');
slides.forEach(slide => {
slide.getPageElements().forEach(el => {
if (el.asShape && el.asShape().getText) {
const tr = el.asShape().getText();
const src = tr.asString();
const dst = src.replace(regex, m => String(map[m] ?? m));
if (src !== dst) tr.setText(dst);
}
});
});
}
// 用法
function runReplace() {
const pres = SlidesApp.openById('你的ID');
replaceAllTexts(pres, { '{NAME}': 'Ivy', '{TEAM}': 'Growth' });
}
安全讀文字工具(避免型別爆炸)
function getAllTexts(slide) {
const out = [];
slide.getPageElements().forEach(el => {
try {
if (el.asShape && el.asShape().getText) {
out.push(el.asShape().getText().asString());
}
} catch (_) {}
});
return out;
}
簡易退避(配額/429 時)
function retry(fn, tries = 5, baseMs = 500) {
let i = 0;
while (true) {
try { return fn(); }
catch (e) {
if (++i >= tries) throw e;
Utilities.sleep(baseMs * Math.pow(2, i)); // 500ms -> 1s -> 2s ...
}
}
}
退避策略是官方建議的處理配額/速率限制通用做法。
