<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Cron on Tarragon</title><link>https://tarrragon.github.io/blog/tags/cron/</link><description>Recent content in Cron on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 06 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/cron/index.xml" rel="self" type="application/rss+xml"/><item><title>時間觸發器：把 raw log 彙總成日報</title><link>https://tarrragon.github.io/blog/automation/04-triggers-automation/time-driven-aggregation/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/automation/04-triggers-automation/time-driven-aggregation/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/automation/knowledge-cards/time-driven-trigger/" data-link-title="Time-Driven Trigger（時間觸發器）" data-link-desc="讓 Apps Script 在固定時間自動執行的排程機制，把被動等呼叫的膠水層變成主動定時跑的任務">時間觸發器&lt;/a>（time-driven trigger）讓 Apps Script 從「被動等 beacon 打進來」變成「主動定時執行」。它的用途是把 raw log 這種逐筆、直接看沒意義的原始資料，每天固定時間彙總成「昨天每篇看幾次」的日報，讓人打開試算表看到的是整理過的數字。先講怎麼設定定時、再講彙總邏輯、最後講怎麼在配額內寫得有效率。&lt;/p>
&lt;h2 id="設定每日定時執行">設定每日定時執行&lt;/h2>
&lt;p>觸發器可以用程式建立，也可以在編輯器左側的「觸發條件」介面點選建立。用程式建立的好處是設定跟著專案走、可版本控制：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">createDailyTrigger&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">ScriptApp&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">newTrigger&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;aggregateYesterday&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">timeBased&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">atHour&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 每天凌晨 1 點
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">everyDays&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">create&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段執行一次（手動按執行），就註冊了一個「每天凌晨 1 點呼叫 &lt;code>aggregateYesterday&lt;/code>」的觸發器。凌晨執行是刻意的：那時流量低、raw log 當天的資料已經齊了，彙總前一天不會漏。要注意 &lt;code>createDailyTrigger&lt;/code> 只該跑一次——每跑一次就多註冊一個觸發器，重複跑會變成一天彙總很多次。管理現有觸發器用 &lt;code>ScriptApp.getProjectTriggers()&lt;/code> 查、&lt;code>deleteTrigger&lt;/code> 刪。&lt;/p>
&lt;h2 id="彙總邏輯">彙總邏輯&lt;/h2>
&lt;p>彙總做的事是：讀 raw log、把同一天同一路徑的瀏覽數出來、寫進另一張「日報」工作表。核心是一個 group by：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">aggregateYesterday&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">ss&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">SpreadsheetApp&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getActiveSpreadsheet&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">raw&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">ss&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getSheetByName&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;工作表1&amp;#34;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">getDataRange&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">getValues&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">report&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">ss&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getSheetByName&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;日報&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nx">ss&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">insertSheet&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;日報&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">counts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{};&lt;/span> &lt;span class="c1">// key = &amp;#34;日期|路徑&amp;#34; → 次數
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nx">raw&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="o">++&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 從第 2 列起，跳過標題
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">var&lt;/span> &lt;span class="nx">d&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">raw&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">];&lt;/span> &lt;span class="c1">// 時間欄
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">var&lt;/span> &lt;span class="nx">day&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">Utilities&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">formatDate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Asia/Taipei&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;yyyy-MM-dd&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">path&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">raw&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">day&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s2">&amp;#34;|&amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">path&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="nx">counts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">key&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">counts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">key&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">rows&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">Object&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">keys&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">counts&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">map&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">k&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">parts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">k&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;|&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nx">parts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nx">parts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nx">counts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">k&lt;/span>&lt;span class="p">]];&lt;/span> &lt;span class="c1">// 日期、路徑、次數
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">rows&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="nx">report&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getRange&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">report&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getLastRow&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">rows&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">setValues&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">rows&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段 group by 有一個前提要明寫出來：&lt;strong>它假設 raw log 裡一列等於一次瀏覽&lt;/strong>。這在模組二的資料模型下成立，因為 beacon 只在頁面載入時送一次。日後若在 payload 加入其他事件型別（例如離開事件，見&lt;a href="https://tarrragon.github.io/blog/automation/06-reading-the-data/event-model/" data-link-title="事件模型與停留時間" data-link-desc="每次瀏覽只記一列時看不出讀者停留多久、有沒有真的在讀；補上離開事件之後，那個秒數的語意與既有報表公式的連帶影響">模組六&lt;/a>），每次瀏覽會產生多列，而這個迴圈仍然合法執行、只是把多數瀏覽數了不只一次——數字膨脹而沒有任何錯誤訊息。膨脹的倍率不固定（離開事件會丟失一部分），所以它比整數倍的錯誤更難從數字本身看出來。屆時的修法是在迴圈裡先篩事件型別：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">raw&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">!==&lt;/span> &lt;span class="s2">&amp;#34;view&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">continue&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 只數進入事件（第 6 欄）
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>把這個前提寫在程式碼旁邊，是為了讓資料模型變更的人搜尋得到所有依賴它的地方。這類「條件式合法執行但已換語意」的失效方式，見&lt;a href="https://tarrragon.github.io/blog/automation/06-reading-the-data/diagnosing-silent-failures/" data-link-title="假故障與靜默失效的診斷" data-link-desc="自建的 Apps Script 流量統計看起來壞了、或看起來正常但數字不對時，分辨症狀出現的位置與問題所在的位置">假故障與靜默失效的診斷&lt;/a>。&lt;/p>
&lt;p>兩個實作決定值得說明。&lt;strong>日期用 &lt;code>Utilities.formatDate&lt;/code> 明確指定時區&lt;/strong>（這裡 &lt;code>Asia/Taipei&lt;/code>），否則跨午夜的資料可能因為時區偏移被算到錯的日子。&lt;strong>寫日報用 &lt;code>setValues&lt;/code> 一次寫一整塊、不用 &lt;code>appendRow&lt;/code> 逐列寫&lt;/strong>——彙總結果可能有幾十上百列，逐列 append 會慢且容易逼近執行時間，一次 &lt;code>setValues&lt;/code> 快得多（呼應&lt;a href="https://tarrragon.github.io/blog/automation/03-sheet-as-database/append-and-concurrency/" data-link-title="寫入與並發：appendRow 與 LockService" data-link-desc="多個 beacon 同時寫進同一張 Sheet 時，appendRow 的競態風險與 LockService 序列化寫入的時機判斷">寫入與並發&lt;/a>講的批次寫入）。&lt;/p>
&lt;h2 id="在-90-分鐘配額內寫得有效率">在 90 分鐘配額內寫得有效率&lt;/h2>
&lt;p>觸發器受一條配額約束：個人帳號所有觸發器每天總執行時間上限 90 分鐘，且單次一樣不能超過 6 分鐘（見&lt;a href="https://tarrragon.github.io/blog/automation/knowledge-cards/execution-quota/" data-link-title="Execution Quota（執行配額）" data-link-desc="Apps Script 個人帳號的執行時間、同時併發與觸發器每日總時間上限，決定免費膠水層能承受多大的量">執行配額&lt;/a>）。上面那段每次 &lt;code>getDataRange().getValues()&lt;/code> 把&lt;strong>整張 raw log&lt;/strong> 讀進來——log 還小時沒問題，但累積到數十萬列後，光讀取就可能逼近 6 分鐘。&lt;/p>
&lt;p>有效率的寫法是&lt;strong>只讀增量&lt;/strong>：記住「上次彙總處理到第幾列」，這次只讀新增的部分。用 &lt;code>PropertiesService&lt;/code> 存這個游標：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">props&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">PropertiesService&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getScriptProperties&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">lastRow&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">Number&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">props&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;lastAggregatedRow&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">sheet&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">ss&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getSheetByName&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;工作表1&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">newCount&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">sheet&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getLastRow&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">lastRow&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">newCount&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">fresh&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">sheet&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getRange&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">lastRow&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">newCount&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">getValues&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 5 = 目前的欄數
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// ... 只彙總 fresh ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">props&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;lastAggregatedRow&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">String&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">sheet&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getLastRow&lt;/span>&lt;span class="p">()));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>那個 &lt;code>5&lt;/code> 是目前的欄數，它與前面那個「一列等於一次瀏覽」是同一種前提：欄位增加時要跟著改，而讀少了不會報錯、只會讓後面的欄位變成 &lt;code>undefined&lt;/code>。&lt;/p>
&lt;p>&lt;strong>兩個常數必須一起改，否則會互相抵銷。&lt;/strong> 前面那個篩選寫的是 &lt;code>raw[i][5] !== &amp;quot;view&amp;quot;&lt;/code>，查的是第六欄；這裡只讀五欄時 &lt;code>raw[i][5]&lt;/code> 恆為 &lt;code>undefined&lt;/code>，條件恆真、每一列都被跳過、日報變成空白。兩處相隔一節、都不產生錯誤，而症狀（日報是空的）指向的位置與成因所在的位置不同。&lt;/p>
&lt;p>只讀增量讓每次彙總的成本跟「昨天新增多少」成正比、而不是跟「歷史總量」成正比，執行時間就穩定、不隨資料累積膨脹。這跟&lt;a href="https://tarrragon.github.io/blog/automation/03-sheet-as-database/data-model-and-capacity/" data-link-title="資料模型與容量邊界" data-link-desc="raw log 表的欄位怎麼設計才好彙總、以及 Sheets 累積到多少列會開始撐不住、撐不住的訊號長什麼樣">資料模型與容量邊界&lt;/a>講的分表是互補的兩招——分表縮小單張表、只讀增量縮小單次讀取，都是為了讓彙總不被歷史總量拖垮。&lt;/p>
&lt;h2 id="下一步">下一步&lt;/h2>
&lt;p>時間觸發器是「到點就跑」的排程。另一類觸發器是「某個事件發生就跑」，例如表單被提交——見&lt;a href="https://tarrragon.github.io/blog/automation/04-triggers-automation/form-and-event-triggers/" data-link-title="表單與事件觸發器" data-link-desc="由 Google 檔案事件（表單提交、試算表編輯）觸發的 Apps Script，以及 simple 與 installable 觸發器在權限上的差別">表單與事件觸發器&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/automation/knowledge-cards/time-driven-trigger/" data-link-title="Time-Driven Trigger（時間觸發器）" data-link-desc="讓 Apps Script 在固定時間自動執行的排程機制，把被動等呼叫的膠水層變成主動定時跑的任務">時間觸發器</a>（time-driven trigger）讓 Apps Script 從「被動等 beacon 打進來」變成「主動定時執行」。它的用途是把 raw log 這種逐筆、直接看沒意義的原始資料，每天固定時間彙總成「昨天每篇看幾次」的日報，讓人打開試算表看到的是整理過的數字。先講怎麼設定定時、再講彙總邏輯、最後講怎麼在配額內寫得有效率。</p>
<h2 id="設定每日定時執行">設定每日定時執行</h2>
<p>觸發器可以用程式建立，也可以在編輯器左側的「觸發條件」介面點選建立。用程式建立的好處是設定跟著專案走、可版本控制：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">createDailyTrigger</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">ScriptApp</span><span class="p">.</span><span class="nx">newTrigger</span><span class="p">(</span><span class="s2">&#34;aggregateYesterday&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="p">.</span><span class="nx">timeBased</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="p">.</span><span class="nx">atHour</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>          <span class="c1">// 每天凌晨 1 點
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>    <span class="p">.</span><span class="nx">everyDays</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="p">.</span><span class="nx">create</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這段執行一次（手動按執行），就註冊了一個「每天凌晨 1 點呼叫 <code>aggregateYesterday</code>」的觸發器。凌晨執行是刻意的：那時流量低、raw log 當天的資料已經齊了，彙總前一天不會漏。要注意 <code>createDailyTrigger</code> 只該跑一次——每跑一次就多註冊一個觸發器，重複跑會變成一天彙總很多次。管理現有觸發器用 <code>ScriptApp.getProjectTriggers()</code> 查、<code>deleteTrigger</code> 刪。</p>
<h2 id="彙總邏輯">彙總邏輯</h2>
<p>彙總做的事是：讀 raw log、把同一天同一路徑的瀏覽數出來、寫進另一張「日報」工作表。核心是一個 group by：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span> <span class="nx">aggregateYesterday</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">var</span> <span class="nx">ss</span> <span class="o">=</span> <span class="nx">SpreadsheetApp</span><span class="p">.</span><span class="nx">getActiveSpreadsheet</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">var</span> <span class="nx">raw</span> <span class="o">=</span> <span class="nx">ss</span><span class="p">.</span><span class="nx">getSheetByName</span><span class="p">(</span><span class="s2">&#34;工作表1&#34;</span><span class="p">).</span><span class="nx">getDataRange</span><span class="p">().</span><span class="nx">getValues</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="kd">var</span> <span class="nx">report</span> <span class="o">=</span> <span class="nx">ss</span><span class="p">.</span><span class="nx">getSheetByName</span><span class="p">(</span><span class="s2">&#34;日報&#34;</span><span class="p">)</span> <span class="o">||</span> <span class="nx">ss</span><span class="p">.</span><span class="nx">insertSheet</span><span class="p">(</span><span class="s2">&#34;日報&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="kd">var</span> <span class="nx">counts</span> <span class="o">=</span> <span class="p">{};</span> <span class="c1">// key = &#34;日期|路徑&#34; → 次數
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>  <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">raw</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>      <span class="c1">// 從第 2 列起，跳過標題
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>    <span class="kd">var</span> <span class="nx">d</span> <span class="o">=</span> <span class="nx">raw</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">0</span><span class="p">];</span>                        <span class="c1">// 時間欄
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span>    <span class="kd">var</span> <span class="nx">day</span> <span class="o">=</span> <span class="nx">Utilities</span><span class="p">.</span><span class="nx">formatDate</span><span class="p">(</span><span class="nx">d</span><span class="p">,</span> <span class="s2">&#34;Asia/Taipei&#34;</span><span class="p">,</span> <span class="s2">&#34;yyyy-MM-dd&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="kd">var</span> <span class="nx">path</span> <span class="o">=</span> <span class="nx">raw</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">1</span><span class="p">];</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="kd">var</span> <span class="nx">key</span> <span class="o">=</span> <span class="nx">day</span> <span class="o">+</span> <span class="s2">&#34;|&#34;</span> <span class="o">+</span> <span class="nx">path</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nx">counts</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="nx">counts</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="o">||</span> <span class="mi">0</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="kd">var</span> <span class="nx">rows</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">counts</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">k</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="kd">var</span> <span class="nx">parts</span> <span class="o">=</span> <span class="nx">k</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="s2">&#34;|&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">return</span> <span class="p">[</span><span class="nx">parts</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="nx">parts</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="nx">counts</span><span class="p">[</span><span class="nx">k</span><span class="p">]];</span>   <span class="c1">// 日期、路徑、次數
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span>  <span class="p">});</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">rows</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="nx">report</span><span class="p">.</span><span class="nx">getRange</span><span class="p">(</span><span class="nx">report</span><span class="p">.</span><span class="nx">getLastRow</span><span class="p">()</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">rows</span><span class="p">.</span><span class="nx">length</span><span class="p">,</span> <span class="mi">3</span><span class="p">).</span><span class="nx">setValues</span><span class="p">(</span><span class="nx">rows</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這段 group by 有一個前提要明寫出來：<strong>它假設 raw log 裡一列等於一次瀏覽</strong>。這在模組二的資料模型下成立，因為 beacon 只在頁面載入時送一次。日後若在 payload 加入其他事件型別（例如離開事件，見<a href="/blog/automation/06-reading-the-data/event-model/" data-link-title="事件模型與停留時間" data-link-desc="每次瀏覽只記一列時看不出讀者停留多久、有沒有真的在讀；補上離開事件之後，那個秒數的語意與既有報表公式的連帶影響">模組六</a>），每次瀏覽會產生多列，而這個迴圈仍然合法執行、只是把多數瀏覽數了不只一次——數字膨脹而沒有任何錯誤訊息。膨脹的倍率不固定（離開事件會丟失一部分），所以它比整數倍的錯誤更難從數字本身看出來。屆時的修法是在迴圈裡先篩事件型別：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nx">raw</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">5</span><span class="p">]</span> <span class="o">!==</span> <span class="s2">&#34;view&#34;</span><span class="p">)</span> <span class="k">continue</span><span class="p">;</span>   <span class="c1">// 只數進入事件（第 6 欄）
</span></span></span></code></pre></div><p>把這個前提寫在程式碼旁邊，是為了讓資料模型變更的人搜尋得到所有依賴它的地方。這類「條件式合法執行但已換語意」的失效方式，見<a href="/blog/automation/06-reading-the-data/diagnosing-silent-failures/" data-link-title="假故障與靜默失效的診斷" data-link-desc="自建的 Apps Script 流量統計看起來壞了、或看起來正常但數字不對時，分辨症狀出現的位置與問題所在的位置">假故障與靜默失效的診斷</a>。</p>
<p>兩個實作決定值得說明。<strong>日期用 <code>Utilities.formatDate</code> 明確指定時區</strong>（這裡 <code>Asia/Taipei</code>），否則跨午夜的資料可能因為時區偏移被算到錯的日子。<strong>寫日報用 <code>setValues</code> 一次寫一整塊、不用 <code>appendRow</code> 逐列寫</strong>——彙總結果可能有幾十上百列，逐列 append 會慢且容易逼近執行時間，一次 <code>setValues</code> 快得多（呼應<a href="/blog/automation/03-sheet-as-database/append-and-concurrency/" data-link-title="寫入與並發：appendRow 與 LockService" data-link-desc="多個 beacon 同時寫進同一張 Sheet 時，appendRow 的競態風險與 LockService 序列化寫入的時機判斷">寫入與並發</a>講的批次寫入）。</p>
<h2 id="在-90-分鐘配額內寫得有效率">在 90 分鐘配額內寫得有效率</h2>
<p>觸發器受一條配額約束：個人帳號所有觸發器每天總執行時間上限 90 分鐘，且單次一樣不能超過 6 分鐘（見<a href="/blog/automation/knowledge-cards/execution-quota/" data-link-title="Execution Quota（執行配額）" data-link-desc="Apps Script 個人帳號的執行時間、同時併發與觸發器每日總時間上限，決定免費膠水層能承受多大的量">執行配額</a>）。上面那段每次 <code>getDataRange().getValues()</code> 把<strong>整張 raw log</strong> 讀進來——log 還小時沒問題，但累積到數十萬列後，光讀取就可能逼近 6 分鐘。</p>
<p>有效率的寫法是<strong>只讀增量</strong>：記住「上次彙總處理到第幾列」，這次只讀新增的部分。用 <code>PropertiesService</code> 存這個游標：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">props</span> <span class="o">=</span> <span class="nx">PropertiesService</span><span class="p">.</span><span class="nx">getScriptProperties</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kd">var</span> <span class="nx">lastRow</span> <span class="o">=</span> <span class="nb">Number</span><span class="p">(</span><span class="nx">props</span><span class="p">.</span><span class="nx">getProperty</span><span class="p">(</span><span class="s2">&#34;lastAggregatedRow&#34;</span><span class="p">)</span> <span class="o">||</span> <span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kd">var</span> <span class="nx">sheet</span> <span class="o">=</span> <span class="nx">ss</span><span class="p">.</span><span class="nx">getSheetByName</span><span class="p">(</span><span class="s2">&#34;工作表1&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="kd">var</span> <span class="nx">newCount</span> <span class="o">=</span> <span class="nx">sheet</span><span class="p">.</span><span class="nx">getLastRow</span><span class="p">()</span> <span class="o">-</span> <span class="nx">lastRow</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nx">newCount</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="kd">var</span> <span class="nx">fresh</span> <span class="o">=</span> <span class="nx">sheet</span><span class="p">.</span><span class="nx">getRange</span><span class="p">(</span><span class="nx">lastRow</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">newCount</span><span class="p">,</span> <span class="mi">5</span><span class="p">).</span><span class="nx">getValues</span><span class="p">();</span>   <span class="c1">// 5 = 目前的欄數
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span>  <span class="c1">// ... 只彙總 fresh ...
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span>  <span class="nx">props</span><span class="p">.</span><span class="nx">setProperty</span><span class="p">(</span><span class="s2">&#34;lastAggregatedRow&#34;</span><span class="p">,</span> <span class="nb">String</span><span class="p">(</span><span class="nx">sheet</span><span class="p">.</span><span class="nx">getLastRow</span><span class="p">()));</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>那個 <code>5</code> 是目前的欄數，它與前面那個「一列等於一次瀏覽」是同一種前提：欄位增加時要跟著改，而讀少了不會報錯、只會讓後面的欄位變成 <code>undefined</code>。</p>
<p><strong>兩個常數必須一起改，否則會互相抵銷。</strong> 前面那個篩選寫的是 <code>raw[i][5] !== &quot;view&quot;</code>，查的是第六欄；這裡只讀五欄時 <code>raw[i][5]</code> 恆為 <code>undefined</code>，條件恆真、每一列都被跳過、日報變成空白。兩處相隔一節、都不產生錯誤，而症狀（日報是空的）指向的位置與成因所在的位置不同。</p>
<p>只讀增量讓每次彙總的成本跟「昨天新增多少」成正比、而不是跟「歷史總量」成正比，執行時間就穩定、不隨資料累積膨脹。這跟<a href="/blog/automation/03-sheet-as-database/data-model-and-capacity/" data-link-title="資料模型與容量邊界" data-link-desc="raw log 表的欄位怎麼設計才好彙總、以及 Sheets 累積到多少列會開始撐不住、撐不住的訊號長什麼樣">資料模型與容量邊界</a>講的分表是互補的兩招——分表縮小單張表、只讀增量縮小單次讀取，都是為了讓彙總不被歷史總量拖垮。</p>
<h2 id="下一步">下一步</h2>
<p>時間觸發器是「到點就跑」的排程。另一類觸發器是「某個事件發生就跑」，例如表單被提交——見<a href="/blog/automation/04-triggers-automation/form-and-event-triggers/" data-link-title="表單與事件觸發器" data-link-desc="由 Google 檔案事件（表單提交、試算表編輯）觸發的 Apps Script，以及 simple 與 installable 觸發器在權限上的差別">表單與事件觸發器</a>。</p>
]]></content:encoded></item></channel></rss>