Try   HackMD
tags: FM631A

Lab 10 更新使用 TDX 運輸資料流通服務

手冊上採用的 PTX 資料服務平台因為政府整合資訊的關係, 在 2022 年底就不再提供服務, 要改成使用 TDX 運輸資料流通服務, 本文將說明如何修改程式改用新的平台獲取相同的資訊。

未註冊用戶的用法

TDX 和 PTX 一樣都可以不註冊使用, 不過會有同一 IP 每日透過程式查詢 50 次的限制, 以下先說明如何使用 TDX 查詢手冊範例中的鐵路班車資訊與特定車站電子看板資訊。

修改瀏覽器字串

要免註冊存取 TDX, 程式一定要加上 TDX 認得的瀏覽器名稱, 以 Firefox 為例, 我們可以如下設定 user-agent:

>>> headers = {'user-agent':'Mozilla/5.0'}

若是照手冊上設定成 curl 工具的名稱, 是會被 TDX 擋掉的。

你可以在慣用的瀏覽器開啟網頁開發者工具, 切換到主控台頁次, 執行 navigator.userAgent 指令, 即可得到瀏覽器的設定名稱:

實際上不需要輸入完整的內容, 只要前面的名稱/版本即可。

修改網址

TDX 的 API 網址和 PTX 幾乎一樣, 只要修改小地方即可:

https://ptx.transportdata.tw/MOTC/v2/Rail/TRA/GeneralTrainInfo/TrainNo/1202?$format=JSON
        ---                  ----
         |                     |
         |                     +-+
         |                       |
         V                       V
        ---                  ---------
https://tdx.transportdata.tw/api/basic/v2/Rail/TRA/GeneralTrainInfo/TrainNo/1202?$format=JSON

就可以存取同樣的服務了。實際的 API 網址可在 TDX 網站上查詢。

簡易測試查詢指定班次的起訖站

這裡以 1202 班次為例, 統合以上所說, 查詢這班次火車的起訖站資訊:

>>> import urequests
>>> headers = {'user-agent':'Mozilla/5.0'}
>>> train_url = 'https://tdx.transportdata.tw/api/basic/v2/Rail/TRA/GeneralTrainInfo/TrainNo/1202?$format=JSON'
>>> train_res = urequests.get(train_url,headers=headers)
>>> j = train_res.json()
>>> print(j[0]['StartingStationName']['Zh_tw'])
北湖
>>> print(j[0]['EndingStationName']['Zh_tw'])
基隆
>>>

修改範例程式

根據以上, 就可以把原始範例改成這樣:

  • lab10_tdx.py

    ​​​​# 無會員:當天次數 50 次
    ​​​​from machine import Pin,PWM
    ​​​​import network     
    ​​​​import urequests   
    ​​​​import time
    ​​​​import tm1637
    ​​​​import ntptime
    
    ​​​​# 四位數顯示器
    ​​​​tm = tm1637.TM1637(clk=Pin(16), dio=Pin(17))
    ​​​​# 清空四位數顯示器
    ​​​​tm.write([0, 0, 0, 0])
    
    ​​​​# 連線至無線網路
    ​​​​sta=network.WLAN(network.STA_IF)
    ​​​​sta.active(True)   
    ​​​​sta.connect('無線網路名稱','無線網路密碼')
    ​​​​while not sta.isconnected() :
    ​​​​    pass
    
    ​​​​print('Wi-Fi連線成功')
    
    ​​​​ntptime.settime()
    
    ​​​​# 想查詢的車號
    ​​​​s_number = 1202
    ​​​​# 想查詢的車站(1000是台北)
    ​​​​sta_number = 1000
    
    ​​​​ifttt_url = "IFTTT請求網址"
    
    ​​​​# 讓網站認為請求是使用瀏覽器發出。因為有些網頁會擋爬蟲程式
    ​​​​headers = {'user-agent':'Mozilla/5.0'}
    
    ​​​​# 查詢指定火車起點與終點
    ​​​​train_url = ("https://tdx.transportdata.tw/api/basic"
    ​​​​            "/v2/Rail/TRA/GeneralTrainInfo/TrainNo/"
    ​​​​            + str(s_number) +"?$format=JSON")
    ​​​​train_res = urequests.get(train_url,headers=headers)
    ​​​​if(train_res.status_code == 200):
    ​​​​    pass
    ​​​​else:
    ​​​​    print("傳送失敗")
    ​​​​    print("錯誤碼:",train_res.status_code)
    
    ​​​​train_j = train_res.json()
    ​​​​print("\n車號:",s_number)
    ​​​​print(train_j[0]['StartingStationName']['Zh_tw'] \
    ​​​​      +" → "+train_j[0]['EndingStationName']['Zh_tw'])
    ​​​​train_res.close()
    ​​​​# 車子第一次出現在時刻表時要提醒使用者
    ​​​​remind = False  
    
    ​​​​# 查詢火車時刻表
    ​​​​time_url = ("https://tdx.transportdata.tw/api/basic"
    ​​​​            "/v2/Rail/TRA/LiveBoard/Station/"
    ​​​​            + str(sta_number)+"?$top=20&$format=JSON")
    
    ​​​​while True:
    ​​​​    time_res = urequests.get(time_url,headers=headers)
    ​​​​    if(time_res.status_code == 200):
    ​​​​        pass
    ​​​​    else:
    ​​​​        print("傳送失敗")
    ​​​​        print("錯誤碼:",time_res.status_code)
    ​​​​    time_j = time_res.json()
    ​​​​    time_res.close()
    ​​​​    # 將車號加入 number列表 中
    ​​​​    number = []
    ​​​​    for i in range(len(time_j)):
    ​​​​        number.append(time_j[i]['TrainNo'])    
    ​​​​    print("\n即時班車號碼:",number)
    
    ​​​​    # 有查到對應車號, 顯示延遲時間
    ​​​​    if(str(s_number) in number):
    ​​​​        if(remind == False):
    ​​​​            # 蜂鳴器
    ​​​​            buzzer_pin = Pin(23,Pin.OUT)
    ​​​​            buzzer = PWM(buzzer_pin,freq=0, duty=50)
    ​​​​            buzzer.freq(349)
    ​​​​            time.sleep(1)
    ​​​​            buzzer.freq(294)
    ​​​​            time.sleep(1)
    ​​​​            buzzer.deinit()
    ​​​​            remind = True
    ​​​​        ind = number.index(str(s_number))
    ​​​​        print("\n表定發車時間:",
    ​​​​              time_j[ind]['ScheduledArrivalTime'])
    ​​​​        print("延遲時間:",
    ​​​​              time_j[ind]['DelayTime'],"分鐘")
    ​​​​        tm.number(time_j[ind]['DelayTime'])
    ​​​​        res = urequests.get(ifttt_url +
    ​​​​                        "?value1=" +
    ​​​​                        str(time_j[ind]['DelayTime']))
    ​​​​        if(res.status_code == 200):
    ​​​​            pass
    ​​​​        else:
    ​​​​            print("傳送失敗")
    ​​​​            print("錯誤碼:",res.status_code)
    ​​​​        res.close()
    ​​​​    # 沒有查到對應車號
    ​​​​    else:
    ​​​​        print("\n目前無"+str(s_number)+"號火車")
    ​​​​        TW_sec = time.mktime(time.localtime())+28800
    ​​​​        TW = time.localtime(TW_sec)
    ​​​​        hour = TW[3]
    ​​​​        minu = TW[4]
    ​​​​        tm.numbers(hour,minu)
    ​​​​        remind = False
    ​​​​    time.sleep(300)  # 暫停 300 秒
    

註冊會員的程式寫法

如果不想受限於每天 50 次查詢, 請先至 TDX 網站註冊會員, 註冊後需要一段時間審核, 審核通過會收到通知信件, 即可登入使用。

找到金鑰資訊

要以會員身份使用 TDX API, 必須知道你的金鑰資訊, 請在登入後的頁面點選建立更多金鑰

往下捲動找到你的預設 API Key, 按一下編輯

就可以看到你的金鑰內含的兩項資訊:

取得 Access Token

實際上要以會員身份使用 TDX API, 必須分成以個階段:

  1. 程式要利用剛剛取得的 Client Id 和 Client Secret 這兩項資訊向 TDX 索取 Access Token, 這個 Access Token 才是最後使用 API 時的認證, 有效期限是 1 天, 也就是說, 程式必須在 Access Token 失效後重新索取。
  2. 利用剛剛取得的 Access Token 當成認證叫用 TDX API

上述過程聽起來就很麻煩, 詳細過程可以參考 〈TDX運輸資料流通服務API介接範例程式碼說明〉, 或是〈「TDX 運輸資料流通服務平台」含 Python 範例程式,PTX 平台的升級版~〉, 我們已經將整個過程包裝成好用的 tdx 模組, 可以大幅簡化程式。

tdx 模組也已經包含在範例檔中, 使用前請先上傳到控制版上。

tdx 模組的用法

tdx 模組使用上很簡單, 步驟如下:

  1. 以 Client Id 及 Client Secret 建立 TDX 物件:

    ​​​​>>> from tdx import TDX
    ​​​​>>> client_id = '你的 Client Id'
    ​​​​>>> client_secret = '你的 Client Secret'
    ​​​​>>> tdx = TDX(client_id, client_secret)
    
  2. 傳入 TDX API 網址呼叫 TDX.get_json() 取得查詢結果轉換後的 JSON 物件:

    ​​​​>>> train_url = 'https://tdx.transportdata.tw/api/basic/v2/Rail/TRA/GeneralTrainInfo/TrainNo/1202?$format=JSON'
    ​​​​>>> j = tdx.get_json(train_url)
    ​​​​get new access token:
    ​​​​200
    ​​​​access token:eyJhbG...
    ​​​​>>> print(j[0]['StartingStationName']['Zh_tw'])
    ​​​​北湖
    ​​​​>>> print(j[0]['EndingStationName']['Zh_tw'])
    ​​​​基隆
    ​​​​>>>
    

是不是很簡單呢?這個物件會在叫用 API 之前確認 Access Token 的有效時間, 若已經過期, 就會自動重新索取。如果查詢出錯, 會傳回一個這樣格式的物件:

{
  'error_code': HTTP 的錯誤碼,
  'text': HTTP 回應內容
}

修改範例程式

有了 tdx 模組, 我們就可以將原本的範例程式改寫, 不管如何使用 TDX API 都沒有問題 (好啦, 其實還是有限制, 每一 IP 每秒 50 次):

  • lab10_tdx_member.py

    ​​​​from machine import Pin,PWM
    ​​​​import network     
    ​​​​import urequests  
    ​​​​import time
    ​​​​import tm1637
    ​​​​import ntptime
    ​​​​from tdx import TDX
    
    ​​​​client_id = '你的 Client Id'
    ​​​​client_secret = '你的 Client Secret'
    
    ​​​​# 想查詢的車號
    ​​​​s_number = 4171
    ​​​​# 想查詢的車站(1000是台北)
    ​​​​# 可以使用以下網址查詢
    ​​​​# https://tip.railway.gov.tw/tra-tip-web/tip/tip001/tip111/view
    ​​​​sta_number = 1000
    
    ​​​​# 四位數顯示器
    ​​​​tm = tm1637.TM1637(clk=Pin(16), dio=Pin(17))
    ​​​​# 清空四位數顯示器
    ​​​​tm.write([0, 0, 0, 0])
    
    ​​​​# 連線至無線網路
    ​​​​sta=network.WLAN(network.STA_IF)
    ​​​​sta.active(True)   
    ​​​​sta.connect('無線網路名稱','無線網路密碼')
    ​​​​while not sta.isconnected() :
    ​​​​    pass
    
    ​​​​print('Wi-Fi連線成功')
    
    ​​​​# 建立 TDX API 存取物件
    ​​​​tdx = TDX(client_id, client_secret)
    
    ​​​​ifttt_url = "IFTTT請求網址"
    
    ​​​​# 查詢指定火車起點與終點
    ​​​​train_url = "https://tdx.transportdata.tw/api/basic/v2/Rail/TRA/GeneralTrainInfo/TrainNo/" \
    ​​​​            + str(s_number) +"?$format=JSON"
    ​​​​train_j = tdx.get_json(train_url)
    ​​​​if('error_code' in train_j):
    ​​​​    print("傳送失敗")
    ​​​​    print("錯誤碼:",train_j['error_code'])
    ​​​​print("\n車號:",s_number)
    ​​​​print(train_j[0]['StartingStationName']['Zh_tw'] \
    ​​​​      +" → "+train_j[0]['EndingStationName']['Zh_tw'])
    ​​​​# 車子第一次出現在時刻表時要提醒使用者
    ​​​​remind = False  
    
    ​​​​# 查詢火車時刻表
    ​​​​time_url = "https://tdx.transportdata.tw/api/basic/v2/Rail/TRA/LiveBoard/Station/" \
    ​​​​            + str(sta_number)+"?$top=20&$format=JSON"
    
    ​​​​while True:
    ​​​​    time_j = tdx.get_json(time_url)
    ​​​​    if('error_code' in time_j):
    ​​​​        print("傳送失敗")
    ​​​​        print("錯誤碼:",time_j['error_code'])    
    ​​​​    # 將車號加入 number列表 中
    ​​​​    number = []
    ​​​​    for i in range(len(time_j)):
    ​​​​        number.append(time_j[i]['TrainNo'])    
    ​​​​    print("\n即時班車號碼:",number)
    
    ​​​​    # 有查到對應車號, 顯示延遲時間
    ​​​​    if(str(s_number) in number):
    ​​​​        if(remind == False):
    ​​​​            # 蜂鳴器
    ​​​​            buzzer = PWM(Pin(23,Pin.OUT),freq=0, duty=512)
    ​​​​            buzzer.freq(349)
    ​​​​            time.sleep(1)
    ​​​​            buzzer.freq(294)
    ​​​​            time.sleep(1)
    ​​​​            buzzer.deinit()
    ​​​​            remind = True
    ​​​​        ind = number.index(str(s_number))
    ​​​​        print("\n表定發車時間:",time_j[ind]['ScheduledArrivalTime'])
    ​​​​        print("延遲時間:",time_j[ind]['DelayTime'],"分鐘")
    ​​​​        tm.number(time_j[ind]['DelayTime'])
    ​​​​        res = urequests.get(ifttt_url + "?value1=" + str(time_j[ind]['DelayTime']))
    ​​​​        if(res.status_code == 200):
    ​​​​            pass
    ​​​​        else:
    ​​​​            print("傳送失敗")
    ​​​​            print("錯誤碼:",res.status_code)
    ​​​​        res.close()
    ​​​​    # 沒有查到對應車號
    ​​​​    else:
    ​​​​        print("\n目前無"+str(s_number)+"號火車")
    ​​​​        TW_sec = time.mktime(time.localtime())+28800
    ​​​​        TW = time.localtime(TW_sec)
    ​​​​        hour = TW[3]
    ​​​​        minu = TW[4]
    ​​​​        tm.numbers(hour,minu)
    ​​​​        remind = False
    ​​​​    time.sleep(300)  # 暫停 300 秒