之前曾寫過Google Apps Script的相關文章 [1] [2],介紹過一些用法,以此為基礎,來開發一個公車應用的後端,對特定站牌的公車,取得預估到達時間,再當成資料來源提供給網頁前端
還沒看過該作品的同學,可以從這邊前往,一鍵公車
首先不得不提一下交通部的德政,將相關資訊統一成一個Web API,並且免費提供,想當年,為了開發公車應用還要跟市政府申請,而且各政府格式可能還不同,PTX只要網路申請,很快就通過可以使用,不過只能從server端呼叫使用,不能直接從前端,這也合理,畢竟機房頻寬資源是有價的,且用且珍惜~
而Google Apps Script剛好可以充當server端的腳色,整體流程如下
- 先在PTX上查好自己需要的路線與站名ID,因為支援OData,可直接在url參數下條件,利用這個特性,即使我需要5個路線都可以用一個url輕鬆完成
- 未來可能會有多種組合,因此使用key來區分
- 藉由UrlFetchApp.fetch方法取得PTX回傳的json資料,這部分稍微卡了一下,有看到別人提問,但還是沒有解答,後來自己摸索出來了
- 因應前端可能密集重複要求,將資料儲存於excel表格內,當成快取使用,時間內用快取,超過時間再重新取得一次
PTX Url組合
我總共有5個路線,10個站點,站點部分因為同個客運公司,相同站的ID也會相同,所以待會看到數量較少是正常的
可以在PTX MOTC這裡先測試他的API及查詢所需的ID,組合url也在這一併完成,公車到站預估主要使用/v2/Bus/EstimatedTimeOfArrival/City/{City}這一組,同縣市可以搜多個路線,利用$filter這個屬性,配合語法可篩選出特定的幾條路線及站別,官方也有提供介紹和範例,我組合出來的$filter長這樣
(RouteUID eq 'TAO713' or RouteUID eq 'TAO715' or RouteID eq 'TAO7151' or RouteUID eq 'TAO716' or RouteUID eq 'TAO717') and (StopID eq '57407' or StopID eq '44378' or StopID eq '57403' or StopID eq '44383')
點擊Try it out按鈕可測試結果是否如預期,並且貼心的附上Request URL,如下
https://ptx.transportdata.tw/MOTC/v2/Bus/EstimatedTimeOfArrival/City/Taoyuan?$filter=(RouteUID eq 'TAO713' or RouteUID eq 'TAO715' or RouteUID eq 'TAO716' or RouteUID eq 'TAO717') and (StopID eq '57407' or StopID eq '44378' or StopID eq '57403' or StopID eq '44383')&$orderby=Direction&$format=JSON
意思是路線包含(TAO713、TAO715、TAO7151、TAO716、TAO717)並且站別包含(57407、44378、57403、44383),並且按照來回程排序,這4個站有2個是我家附近前往永寧,另外2個則是永寧搭車回來的,這樣我可以知道提早多久出門就好,也可以知道永寧那邊要先排哪一路比較快發車
快取excel格式如下

包含url當key值,快取內容,快取時間用來判斷是否需要更新,順便統計次數,最後是方便我自己看的最後更新時間,這個結構對任意url都可以快取,還蠻方便的~
//excel的resource id
const SHEET_ID = "Your Sheet ID";
//sheet名稱,就是下方的分頁
const SHEET_PAGE_CACHE = "UrlCache";
const KEY_BADE_BUS = "Custom key";
var SpreadSheet = SpreadsheetApp.openById(SHEET_ID);
var UrlCacheSheet = SpreadSheet.getSheetByName(SHEET_PAGE_CACHE);
function doGet(e){
var key = e.parameter.key;
//使用key來區分不同組合
switch(key){
case KEY_BADE_BUS:
var value = GetBadeBus();
return ContentService.createTextOutput(value)
.setMimeType(ContentService.MimeType.JSON);
}
return ContentService.createTextOutput('[]')
.setMimeType(ContentService.MimeType.JSON);
}
/*針對713、715、716、717惠爾德站*/
function GetBadeBus(){
//和上面長的不一樣是因為經過urlencode,實測沒有urlencod也可以正常運行
var url = "https://ptx.transportdata.tw/MOTC/v2/Bus/EstimatedTimeOfArrival/City/Taoyuan?$filter=(RouteUID%20eq%20'TAO713'%20or%20RouteUID%20eq%20'TAO715'%20or%20RouteUID%20eq%20'TAO7151'%20or%20RouteUID%20eq%20'TAO716'%20or%20RouteUID%20eq%20'TAO717')%20and%20(StopID%20eq%20'57407'%20or%20StopID%20eq%20'44378'%20or%20StopID%20eq%20'57403'%20or%20StopID%20eq%20'44383')&$orderby=Direction&$format=JSON";
//確認是否有60秒內的快取
var cache_data = GetCache(url, 60);
if(cache_data != ""){
return cache_data;
}
else{
var options = {
'headers': GetAuthorizationHeader(),
'method': 'get'
}
var getans=UrlFetchApp.fetch(url);
if (getans.getResponseCode() == 200 ){
var cache_no = GetCacheNo(url);
//判斷是否已寫入過該url的快取,決定要更新還是寫入
if(cache_no > 1){
var count = parseInt(UrlCacheSheet.getSheetValues(cache_no, 4, 1, 1)[0], 10);
InsertCache(cache_no, url, getans.getContentText(), count+1);
}else{
var last_row = UrlCacheSheet.getLastRow();
InsertCache(last_row+1, url, getans.getContentText(), 1);
}
return getans.getContentText();
}
}
return "[]";
}
/**
* 根據url從快取取得資料,並且限定資料時間在幾秒內,若超過時間或找不到回傳空字串
*/
function GetCache(url, seconds_ago){
var last_row = UrlCacheSheet.getLastRow();
if(last_row > 1){
var data = UrlCacheSheet.getSheetValues(2, 1, last_row-1, 3);
for(var i=0;i<data.length;i++){
if(url == data[i][0]){
var last = new Date(parseInt(data[i][2], 10));
var now = new Date();
if(Math.abs(now - last)/1000 < seconds_ago){
return data[i][1];
}
}
}
}
return "";
}
/**
* 由url來取得row no,無則回傳0
*/
function GetCacheNo(url){
var last_row = UrlCacheSheet.getLastRow();
if(last_row > 1){
var data = UrlCacheSheet.getSheetValues(2, 1, last_row-1, 3);
for(var i=0;i<data.length;i++){
if(url === data[i][0]){return (i + 2);}
}
}
return 0;
}
/**
* 由row no來決定更新或寫入快取
*/
function InsertCache(no, url, content, count){
UrlCacheSheet.getRange(no, 1).setValue(url);
UrlCacheSheet.getRange(no, 2).setValue(content);
var now = new Date();
UrlCacheSheet.getRange(no, 3).setValue(now.getTime());
UrlCacheSheet.getRange(no, 4).setValue(count);
UrlCacheSheet.getRange(no, 5).setValue(now.toString());
}
function GetAuthorizationHeader() {
var AppID = 'Your App ID';
var AppKey = 'Your App Key';
var GMTString = new Date().toGMTString();
var ShaObj = new jsSHA('SHA-1', 'TEXT');
ShaObj.setHMACKey(AppKey, 'TEXT');
ShaObj.update('x-date: ' + GMTString);
var HMAC = ShaObj.getHMAC('B64');
var Authorization = 'hmac username=\"' + AppID + '\", algorithm=\"hmac-sha1\", headers=\"x-date\", signature=\"' + HMAC + '\"';
return { 'Authorization': Authorization, 'X-Date': GMTString};
}
其中GetAuthorizationHeader這個方法會用到Sha演算法,我是直接從官網提供的開發範例javascript中sha.js複製過來使用,從GAS編輯器上的檔案=>新增=>指令碼檔案 再貼上即可。
後續會陸續更新前端的部分,敬請期待~
參考