Google 簡報: 打開指定 ID 的簡報 openById()

 


在做簡報自動化時,第一個繞不開的關卡,就是怎麼正確「打開那一份指定的簡報」。

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 ...
    }
  }
}


退避策略是官方建議的處理配額/速率限制通用做法。


張貼留言 (0)
較新的 較舊