前面幾天講的都是方法跟原則,今天換個口味,直接做一個能跑的東西——一個把圖書館通知信自動變成日曆提醒的小程式。
挑這個當第一個示範,不是因為它多有用,是因為它把一個自動化該有的零件全部用上了,但資料完全不敏感,就算哪裡寫錯也不會有任何後果,最多就是少收到一次提醒。你可以直接照著抄一次,把授權、觸發、去重這幾關走過一遍,之後不管做什麼系統,這套機制都用得上,不用每次都重新摸索一次。
臺北市立圖書館借的書快到期,會寄一封「借閱即將到期通知單」的信。這封信我以前都是滑手機的時候剛好瞄到才想起來要還書,逾期罰款繳過不只一次,每次金額不大,但累積起來也是白白浪費的錢。目標很單純:信一到,自動在日曆上加一則提醒,提前一天跳出來,往後這種小罰款就不用再繳了。
任何一個自動化,拆開來看大概都是這五個動作:多久檢查一次算是觸發、去哪裡找符合條件的東西算是搜尋、從裡面讀出我要的資訊算是解析、拿到資訊之後做什麼算是動作,還有怎麼確保同一件事不會被處理兩次算是去重。這五件事想清楚,接下來不管換成哪一種自動化,骨架都是同一套,差別只在每一段裡面填的內容。
工具選 Google Apps Script,不是隨便挑的:這件事就是在 Gmail 跟日曆之間搬東西,不需要長時間運算,也不用另外架一台機器讓它整天開著,剛好落在它最適合的位置上。
寫成 Google Apps Script,大概是這樣:
var LABEL_NAME = '已處理-圖書館通知';
var SENDER_DOMAIN = 'newWebcat@email.tpml.edu.tw';
var SUBJECT_KEYWORD = '借閱即將到期通知單';
function setupHourlyTrigger() {
ScriptApp.newTrigger('processLibraryNotices').timeBased().everyHours(1).create();
}
function processLibraryNotices() {
var label = GmailApp.getUserLabelByName(LABEL_NAME) || GmailApp.createLabel(LABEL_NAME);
var query = 'from:(' + SENDER_DOMAIN + ') subject:"' + SUBJECT_KEYWORD + '" -label:"' + LABEL_NAME + '"';
GmailApp.search(query, 0, 20).forEach(function (thread) {
var body = thread.getMessages()[0].getPlainBody();
var d = /原到期日期[::]\s*(\d{4}-\d{2}-\d{2})/.exec(body);
var t = /書名[::]\s*(.+)/.exec(body);
if (!d || !t) {
MailApp.sendEmail(Session.getActiveUser().getEmail(), '解析失敗', thread.getFirstMessageSubject());
return;
}
CalendarApp.getDefaultCalendar().createAllDayEvent('還書提醒:' + t[1], new Date(d[1])).addPopupReminder(1440);
thread.addLabel(label);
});
}
setupHourlyTrigger 只要手動執行一次,讓它每小時檢查一次信箱,這就是觸發。processLibraryNotices 才是主體:用寄件者跟主旨關鍵字搜尋還沒處理過的信,這是搜尋;抓出書名跟到期日,這是解析;建一則全天日曆事件、提前一天提醒,這是動作;最後貼上標籤,下次搜尋自動排除已經處理過的信,這就是去重。五個零件,一個都沒少,而且加起來還不到三十行,比大部分人想像中的自動化短得多。
老實講,第一次照抄這段的時候我也漏改過 SENDER_DOMAIN,結果查了半天為什麼搜尋不到任何一封信——這種低級錯誤幾乎每次抄現成程式碼都會遇到一次,改完重新手動跑一次就好,不用太緊張。
第一次執行會跳出「這個應用程式未經 Google 驗證」的警告畫面,看起來有點嚇人。這是因為腳本還沒送 Google 官方審核,屬於自己寫給自己用的腳本正常會遇到的畫面,點進階、選擇繼續執行即可,不代表帳號有任何風險,也不需要真的去申請審核。
第二件事是先手動執行一次 processLibraryNotices,而不是設好觸發器就放著不管。手動跑一次可以立刻看到 Google 的執行紀錄,確認搜尋條件真的找到信、日期跟書名真的解析對了,這比等一整個小時之後才發現哪裡寫錯要有效率得多。
第三件事,也是程式碼裡最容易被忽略但最重要的一段:解析不到日期或書名的時候,我選擇直接放棄這封信、寄一封通知信給自己,而不是硬猜一個日期填進去。錯的提醒比沒有提醒更糟——你會信任一個其實是亂猜的日期,反而錯過真正的期限。這封信也故意不貼標籤,代表下一輪還會被重新檢查,一旦哪天信件格式改回來能解析了,它自己就會補上,不需要我手動處理。
正式讓它每小時自動跑之前,我會先寄一封格式一樣、但收件人是自己的假通知信,確認整條路徑真的走得通,而不是只看程式碼有沒有語法錯誤。程式碼能編譯過,跟它在真實世界裡真的做對事,完全是兩回事——很多問題只有拿真實格式的資料跑過一次才會冒出來,光用眼睛讀程式碼是看不出來的。
上面這段大概三十幾行,是教學用的簡化版本。我們現在真正在跑的那支,因為多支援了一種通知類型、要解析信件裡的完整表格、還要處理「同一封信失敗了不要每小時重複通知」這類細節,長到三百多行。
這是所有自動化都會發生的事:能動的雛形很快就寫得出來,真正花時間的是後來一個一個補上的例外處理。但雛形先能動,才知道接下來要補什麼——如果一開始就想把三百行的完整版本一次寫對,反而會卡在還沒驗證過的細節上動彈不得。真正跑起來之後才會冒出來的狀況,永遠比坐在那裡想像的多,先讓最單純的版本能動,剩下的例外之後遇到再一個一個補。
會用到的服務都是 Google 帳號內建的功能——Gmail、日曆、指令碼——不需要另外申請任何金鑰,也不需要額外付費,這是挑這個題目當第一課的另一個原因:讓你先把整套流程走熟,還不用先煩惱金鑰跟費用怎麼處理。
第一個自動化,別挑公司最痛的那個。挑一個像這樣「壞了也沒關係」的小事,把授權怎麼跳、觸發器怎麼設、去重怎麼做這三關先走過一次。等這個熟了,之後每加一個自動化,要煩惱的只剩「這件事的規則是什麼」,不用再重新搞懂機制怎麼接。
這一支花不到一個晚上就能做完,說實在也幫你省下忘記還書的那幾塊罰款——我知道投入跟回報不成比例,但正是這種不成比例,才是拿來練習最划算的題目。