Side Project - 串接高鐵API查詢、Line Bot == > 專案開始時間:2024/10/18 > 參考頁面: > [Maso的萬事屋 - 用 Django 架構建置專屬的LINEBOT吧 系列文章](https://ithelp.ithome.com.tw/m/users/20121176/ironman/3023) ## 專案發想 要搭乘高鐵時需額外打開高鐵app查詢班次,但因為不是我的常用app,需額外去搜尋這個app來用,但好不容易打開高鐵app後卻發現沒有剩餘車票~~~ 如果可以先在line裡面搜尋確認有票再做訂票的話,也許會比較方便?(是多懶惰!!!) - 希望可以快速在line上查到車次時間、剩餘車票及票價 - 除了起點站及終點站外,希望可以查詢第三靠站地點車次 - 有同行旅客想搭乘同班車但上下車站點不同 ## 預計開發功能 ### 短期計劃 - Line Messaging API 串接 - [x] 申請帳號 - 2024/10/18 - [x] 連線測試 - 2024/10/19 - TDX API 串接 - [x] 申請TDX帳號 - 2024/10/22 ( 通過 ) - 票價查詢功能 - [x] 查詢起訖站間票價(文字) - 起點 & 終點 - 2024/10/24 - 車廂等級 - 2024/10/24 - 票種 - 單程 / 來回票 - [ ] 模板訊息( LINE 內建功能) - 車次查詢功能 - [ ] 查詢出發時刻班次 - [ ] 查詢抵達時刻班次 - [ ] 模板訊息( LINE 內建功能) - 剩餘車票查詢功能 - [ ] 指定車次剩餘車票 - [ ] 指定時間剩餘車票 - 部署 - [ ] AWS 部署 ### 中長期計畫 - 訂票功能 - [ ] 考量安全性問題 - 進階查詢 - [ ] 除起點終點兩個站點之外,額外指定第三靠站地點 ## 使用技術 - **語言 / 框架**:Python / Django - **API串接**:TDX API、Line Messaging API - **使用套件**: ## 開發紀錄 ### LOG - **2024/10/18** - 開啟專案、LINE MESSAGING API 串接、TDX 帳號申請(待審核) - **2024/10/19** - 筆記整理、測試+調整 LINE BOT、TDX帳號待審核 - **2024/10/21** - TDX帳號審核未過,重新申請 - **2024/10/22** - TDX帳號審核通過 - **2024/10/24** - 票價查詢功能(純文字)完成 - ### 建立 Django 專案 #### 1. [[note] Django 專案建立](/iatsXQi9Qk2WNPAbM2faNA) - <font color="red"> app 名稱**不要**直接取 linebot</font>, 會和 **line-bot-sdk** 套件的內建模組衝突 #### 2. 安裝套件: - `$ poetry add line-bot-sdk` #### 3. core/settings.py 及 .env 檔 - 在 settings 檔內放入<font color="orange">**LINE_CHANNEL_ACCESS_TOKEN**</font> 和 <font color="orange">**LINE_CHANNEL_SECRET**</font> 欄位,並一併將內容放入 ++.env++ 檔 - 取得 token 及 secret : [[note] Line Messaging API 串接](/Vn_2nkahSK6myP7tbH79vg) - 建議放置在 **SECRET_KEY** 前,方便管理 - 記得先安裝 `python-dotenv` 套件 ```python # core/settings.py from dotenv import load_dotenv load_dotenv() LINE_CHANNEL_ACCESS_TOKEN = os.getenv("LINE_CHANNEL_ACCESS_TOKEN") LINE_CHANNEL_SECRET = os.getenv("LINE_CHANNEL_SECRET") SECRET_KEY = os.getenv("SECRET_KEY") # .env LINE_CHANNEL_ACCESS_TOKEN = "your_token" LINE_CHANNEL_SECRET = "your_secret" SECRET_KEY = "your_secret_key" ``` ### ngrok 設定 [[note] ngrok 設定](/ur2OVmZQTtq7TWwgxBOMNw) ### API串接 1. **TDX API** [[note] TDX API 串接](/EbeN3QZSTyuQ9niEhapyOQ) 2. **Line Messaging API** [[note] Line Messaging API 串接](/Vn_2nkahSK6myP7tbH79vg) ### 訊息事件處理 語法筆記 #### signature ```python signature = request.META["HTTP_X_LINE_SIGNATURE"] ``` - 用於驗證請求的安全性,防止偽造請求的攻擊 - `HTTP_X_LINE_SIGNATURE` 是 LINE 伺服器發送的請求中的一個標頭,用於驗證請求是否來自 LINE - **和 token 作用類似**,但不同: - ++**signature**++ 是一個用於驗證請求來源的數位簽名,++屬於單一請求的驗證++ - ++**token**++ 則是用來做身份驗證及授權,++是持久性的++ #### request.body ```python body = request.body.decode("utf-8") ``` - body 的作用是讓我們取得LINE伺服器發送的事件資料,例如訊息、按鈕點擊等 - 先取得 body,後續去解析為 JSON 格式後,以便取得事件類型、用戶ID、訊息內容等資訊 - `request` 在 Django 框架中代表 HTTP 請求 - `request.body` 是一個屬性,包含了請求的原始內容 - `decode("utf-8")` 是將字節串 (Bytes,是二進制的數據) 轉為字符串 (String) ### 遇到的問題 (未解決) #### 1. class-based-views 路徑無法與 webhook url 連接 - 原本想將 views.py 內 LineBot 相關的 function 都移至 class 內做統一管理,但 webhook url 會無法確實連線,所以改回原本 function-based-views 寫法 - 寫在 class 內的 function 要多帶一個參數 self(可用其他名稱),self 會帶入實體本身,不確定是不是這個因素才無法連線 #### 2. OData 語法 - filter 篩選條件只要符合單一條件的元素就會被傳回 ```python # 取得票價 # fare_class 預設為 1:成人票,ticket_type 預設為 1:單程票 def get_ticket_price(self, station_id, destination_id, cabin_class, fare_class=1, ticket_type=1): url = f"{self.basic_url}/Rail/THSR/ODFare/{station_id}/to/{destination_id}" params = { "$select": "Fares,OriginStationName,DestinationStationName", # OData 的 filter 語法,從服務器端過濾數據,只有符合條件的數據會被傳回 # 比在客戶端過濾更高效,但只要符合單一條件的票價就會被傳回(不確定原因,因為已經用 and 連接) "$filter": f"Fares/any(f: f/CabinClass eq {cabin_class} and f/FareClass eq {fare_class} and f/TicketType eq {ticket_type})", "$format": "JSON", } try: response = self.get_response(url, params) response.raise_for_status() ticket_data = response.json() if isinstance(ticket_data, list) and len(ticket_data) > 0: # 從 ticket_data 中取出第一個元素,並取得 Fares 欄位的值,如果不存在則返回空列表 fares = ticket_data[0].get("Fares", []) # 在客戶端過濾,較靈活,會將符合條件的票價存入 filtered_fares filtered_fares = [fare for fare in fares if fare.get("CabinClass") == cabin_class and fare.get("FareClass") == fare_class and fare.get("TicketType") == ticket_type] return filtered_fares[0]["Price"] else: print("未找到對應的票價") return None except requests.exceptions.HTTPError as http_error: print(f"HTTP error occurred: {http_error}") except Exception as error: print(f"An error occurred: {error}") return None ``` - 雖然在 OData 語法已經給 $filter 條件,但得到以下內容 ``` ticket_data: [{ 'OriginStationName': {'Zh_tw': '南港', 'En': 'Nangang'}, 'DestinationStationName': {'Zh_tw': '板橋', 'En': 'Banqiao'}, 'Direction': 0, 'Fares': [ {'TicketType': 1, 'FareClass': 1, 'CabinClass': 1, 'Price': 70}, {'TicketType': 1, 'FareClass': 9, 'CabinClass': 3, 'Price': 30}, {'TicketType': 1, 'FareClass': 9, 'CabinClass': 1, 'Price': 35}, {'TicketType': 1, 'FareClass': 1, 'CabinClass': 3, 'Price': 65}, {'TicketType': 1, 'FareClass': 9, 'CabinClass': 2, 'Price': 155}, {'TicketType': 1, 'FareClass': 1, 'CabinClass': 2, 'Price': 310}, {'TicketType': 8, 'FareClass': 1, 'CabinClass': 1, 'Price': 65}, {'TicketType': 8, 'FareClass': 1, 'CabinClass': 2, 'Price': 290} ], 'SrcUpdateTime': '2024-06-07T20:05:43+08:00', 'UpdateTime': '2024-10-04T12:15:17+08:00', 'VersionID': 48 }] ```