# Linebot 進階 - 小專案課程2
:::warning
### 上一堂課
https://hackmd.io/@k0217/rJKNMyhDkx
:::
---
### 今日課程
* 目標:使用 Linebot
* 自動回覆(Line 官方帳號設定)
* 網路搜尋(API)
* 圖表(Python 套件)
* Gemini 回覆(API)
* 示範案例:開發一個可以回應課表、查詢附近食物、股票以及使用 Gemini AI 去生成回應的 Line bot
* 技術:Line、Google API、Python 套件
---
## 第零步:設定今日課程(可參考上次課程)
使用以下圖片為圖文選單

並依序點擊回應文字
* 課表
* 附近的餐廳
* 台積電股票
若是沒有產生回覆,可以至 render 的網站,點擊 「Deploy latest commit」,若是希望可以長期運轉不休眠,可以試試 `UptimeRobot`

:::spoiler **UptimeRobot 簡介**
#### 什麼是 UptimeRobot?
**UptimeRobot** 是一個網站監測工具,能定期檢查網站是否正常運行,並在網站當機時發送通知。它適合用來監控 API、伺服器或個人網站的可用性。
#### 主要功能
- **網站監測**:支援 HTTP(s)、Ping、Port(TCP)、Keyword 監測方式
- **通知機制**:當網站無法訪問時,可透過 Email、Telegram、Discord、Slack、LINE 等方式通知
- **免費方案**:每 5 分鐘檢測一次,最多監測 50 個網址
- **付費方案**:更短的檢測間隔(最短 30 秒)及更多通知方式
#### 使用方式
1. **註冊 UptimeRobot 帳號**:[UptimeRobot 官網](https://uptimerobot.com/)
2. **新增監測**:輸入網站 URL,選擇監測類型與時間間隔
3. **設定通知**:選擇 Email 或第三方工具接收異常通知
#### 適用場景
- 監控個人網站、API、伺服器運行狀況
- 保持免費伺服器(如 Vercel、Railway)存活
- 提早發現網站掛掉問題,確保穩定運行
:::
---
## 1. 自動回覆(使用課表舉例)
:::info
### 📌 自動回覆訊息
**自動回覆訊息** 是 LINE Bot 內建的功能,能夠根據預設的條件回應使用者訊息,無需伺服器支援。自動回覆可設定為:
- **純文字回應**(如問候語、公告資訊)
- **圖片、影片、貼圖回應**(提供視覺化資訊)
- **按鈕選單回應**(讓使用者選擇進一步操作)
此功能適合用於 **基本客服、營運公告、常見問題回覆**,但無法處理動態內容或外部資料查詢。
---
### 📌 限制與注意事項
1. **與 Webhook 互斥**:如果啟用 Webhook,LINE Developers 的 **自動回覆功能** 可能會出現錯誤。
2. **關鍵字設計需謹慎**:過於簡單的關鍵字可能導致誤觸,建議使用 **部分比對 + 關鍵字組合** 或 **正則表達式** 來提升準確度。
3. **免費方案訊息限制**:LINE 官方帳號的 **免費方案** 每月有訊息數量上限,超過後將無法回應。
4. **不適用於複雜邏輯**:若需要 **動態回應、外部 API 查詢、個人化內容**,應使用 Webhook 方式處理。
透過適當的 **關鍵字設計與回應設定**,可確保 LINE Bot 能夠有效提供資訊,提高使用者互動體驗。
:::
### 第一步:開啟「自動回覆」
進入 [Line Developers](https://developers.line.biz/zh-hant/) 並開啟自己的帳號
點擊至 Messaging API,下滑至 LINE Official Account features

點擊 Auto-reply messages 後面的 Edit
進到 Line official account manager 開啟「自動回應訊息」

### 第二步:設定自動回覆
回到 Line Official Account Manager 首頁
點擊左側「自動回應訊息」

關閉原本的預設自動回覆

建立新回覆

「關鍵字回應」可依你的需求,選擇會觸發的關鍵字
下方可選擇回應類型,這裡先使用文字以及圖片

點擊建立後即可完成自動回覆


## 2. 網路搜尋(API)(以附近餐廳為例)
### 初始專案介紹
#### 檔案介紹:
:::spoiler 檔案介紹
將 github 的檔案下載至本地,並使用 VScode 打開
資料夾內容:

```
.env.example //環境變數範例
.gitignore //不上傳到 github 的檔案
app.py //主要的程式
README.md //專案說明
requirements.txt //需下載資源
```
要在本地用,需要依據 .env.example 檔,新增一個 .env 檔
:::
#### 初始程式碼介紹:
:::spoiler `app.py`介紹
#### 1. 環境變數與套件導入
```python=
import os
from dotenv import load_dotenv
from flask import Flask, request, abort
from linebot.v3.webhook import WebhookHandler, Event
from linebot.v3.exceptions import InvalidSignatureError
from linebot.v3.messaging.models import TextMessage
from linebot import LineBotApi, WebhookHandler
from linebot.models import (
MessageEvent,
TextMessage,
TextSendMessage,
ImageSendMessage)
from linebot.exceptions import InvalidSignatureError
import logging
```
說明
* os:用於讀取環境變數。
* dotenv:load_dotenv() 可加載 .env 文件中的環境變數。
* flask:導入 Flask 相關函式。
* linebot.v3:LINE 官方 SDK,用於處理 LINE Bot 相關功能。
* logging:設定應用的日誌級別。
---
#### 2. 環境變數設置
```python=
# 加載 .env 文件中的變數
load_dotenv()
# 從環境變數中讀取 LINE 的 Channel Access Token 和 Channel Secret
line_token = os.getenv('LINE_TOKEN')
line_secret = os.getenv('LINE_SECRET')
# 檢查是否設置了環境變數
if not line_token or not line_secret:
print(f"LINE_TOKEN: {line_token}") # 調試輸出
print(f"LINE_SECRET: {line_secret}") # 調試輸出
raise ValueError("LINE_TOKEN 或 LINE_SECRET 未設置")
```
說明
* load_dotenv():從 .env 文件讀取 LINE_TOKEN 和 LINE_SECRET。
---
#### 3. 初始化 LINE Bot API
```python=
line_bot_api = LineBotApi(line_token)
handler = WebhookHandler(line_secret)
```
說明
* LineBotApi:用於與 LINE 平台互動,如發送訊息。
* WebhookHandler:用於處理 LINE 的 Webhook 事件。
---
#### 4. 建立 Flask 伺服器
```python=
app = Flask(__name__)
app.logger.setLevel(logging.DEBUG)
```
說明
* Flask(__name__):初始化 Flask 應用。
* 設置 logging.DEBUG 以顯示詳細日誌。
---
#### 5. 設置 Webhook
```python=
@app.route("/", methods=['POST'])
def callback():
# 取得 X-Line-Signature 標頭
signature = request.headers['X-Line-Signature']
# 取得請求的原始內容
body = request.get_data(as_text=True)
app.logger.info(f"Request body: {body}")
# 驗證簽名並處理請求
try:
handler.handle(body, signature)
except InvalidSignatureError:
abort(400)
return 'OK'
```
說明
* @app.route("/", methods=['POST']):建立 POST 請求的 Webhook。
* request.headers['X-Line-Signature']:取得 LINE 傳來的簽名,用於驗證請求來源。
* request.get_data(as_text=True):取得請求的 JSON 內容。
* handler.handle(body, signature):處理 LINE 事件。
---
#### 6. 訊息事件處理
```python=
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event: Event):
if event.message.type == "text":
user_message = event.message.text # 使用者的訊息
app.logger.info(f"收到的訊息: {user_message}")
reply_text = ("你說了:" + user_message)
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text=reply_text)
)
```
說明
* @handler.add(MessageEvent, message=TextMessage):監聽 TextMessage 事件。
* event.message.text:取得使用者傳來的文字訊息。
* line_bot_api.reply_message(event.reply_token, TextSendMessage(text=reply_text)):回覆使用者。
---
#### 7. 啟動 Flask 伺服器
```python=
if __name__ == "__main__":
app.run(host='0.0.0.0', port=5000)
```
說明
* if __name__ == "__main__":確保程式是直接執行,而非作為模組導入。
* app.run(host='0.0.0.0', port=5000):啟動 Flask 伺服器,監聽所有 IP 地址的 5000 端口。
:::
---
### 使用 Google API 去找到附近餐廳(以台北101為例)
:::info
### 📌 前置知識點
#### 🔹 Google Places API 簡介
Google Places API 是 Google 提供的一個強大工具,能夠查詢 **附近的地點資訊**,例如:
- 餐廳
- 便利商店
- 觀光景點
- 公交站點
當我們希望透過 **LINE Bot 查詢附近餐廳**,可以使用 **Places API 的 `nearbysearch` 功能**,根據:
- **使用者的位置**(經緯度)
- **指定類別(如餐廳 `restaurant`)**
- **篩選條件(距離、評分等)**
來獲取附近的餐廳資訊。
#### 🔹 API 調用的基本流程(Python + `requests` 套件)
在 Python 中,我們可以透過 **`requests` 套件** 發送 HTTP 請求來獲取 Google Places API 的回應:
1. 先取得 **Google API Key**
2. 使用 `requests.get()` 呼叫 API
3. 解析回傳的 **JSON 資料**
4. 依據需要的條件進行篩選
#### 🔹 API 回傳的資料格式
Google Places API 會回傳一個 JSON 格式的清單,每個地點包含:
- name: 餐廳名稱
- rating: 評分
- user_ratings_total: 評價數量
- geometry.location.lat / geometry.location.lng: 經緯度
- vicinity: 地址
:::
### 第一步:申請 Google Cloud 專案
進入 [Google Cloud](https://cloud.google.com/?hl=zh-TW),點擊右上角「控制台」,並新建專案

點擊「API和服務」

查詢並啟用 Places API(這裡會需要輸入卡號,但只要不要亂按就不會收費)

輸入完成後,紀錄你的 API KEY
- [ ] 簡報介紹 API
使用此 apikey 即可搜尋該位置(台北101)的附近餐廳
```
https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=25.033964,121.564472&radius=1000&type=restaurant&key=你的API_KEY&language=zh-TW
```
:::info
可使用 postman 去測試
:::
### 第二步:修改 github 專案
1. 新增環境變數(在 render 新增一個 GOOGLE_PLACES_API_KEY 的環境變數,並放入剛剛的 APIKEY)

2. 在專案中新增一個叫 `places.py` 的檔案,用來使用 api 去取用餐廳資訊
```python=
import requests
from dotenv import load_dotenv
import os
# 加載 .env 文件中的變數
load_dotenv()
# 你的 Google Places API Key
API_KEY = os.getenv("GOOGLE_PLACES_API_KEY")
def get_nearby_restaurants():
""" 查詢附近的餐廳資訊 """
latitude, longitude, radius = 25.033964, 121.564472, 1000 # 台北101
url = (f"https://maps.googleapis.com/maps/api/place/nearbysearch/json"
f"?location={latitude},{longitude}&radius={radius}&type=restaurant&key={API_KEY}&language=zh-TW")
response = requests.get(url).json()
results = []
if "results" in response:
for place in response["results"][:5]:
name = place["name"]
rating = place.get("rating", "N/A")
address = place.get("vicinity", "未知地址")
place_id = place["place_id"]
map_url = f"https://www.google.com/maps/place/?q=place_id:{place_id}"
results.append(f"🍽 {name} | ⭐ {rating}\n📍 {address}\n🔗 {map_url}")
return "\n\n".join(results) if results else "找不到餐廳資訊 😢"
```
3. 在原本的 `app.py` 取用 `places.py` 的 function,用於 line 回覆
* 最上方新增`from places import get_nearby_restaurants`
* 將`reply_text = ("你說了:" + user_message)`修改為
```python=
if user_message == "附近的餐廳":
reply_text = get_nearby_restaurants()
elif user_message == "課表":
reply_text = "這是你的課表~"
else:
reply_text = ("你說了:" + user_message)
```
4. 將檔案推上 github

## 3. 圖表(以台積電股票為例)
:::info
### Python套件介紹
numpy:數值運算
matplotlib:繪製圖表
twstock:台灣股票資訊查詢
pandas:處理結構化數據
cloudinary:圖片與影片的雲端儲存與管理
:::
:::info
### 📌前置知識點(不需實作)
#### 🔹 1. 環境變數與 dotenv
在程式碼中,我們使用 `.env` 檔案來存放 **Cloudinary API 金鑰**,並透過 `dotenv` 套件讀取它。
##### `.env` 檔案格式
```=
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
```
#### 📌 Python 讀取 `.env`
```python
import os
from dotenv import load_dotenv
load_dotenv()
cloud_name = os.getenv('CLOUDINARY_CLOUD_NAME')
api_key = os.getenv('CLOUDINARY_API_KEY')
api_secret = os.getenv('CLOUDINARY_API_SECRET')
```
🔹 應用場景:讓敏感資訊不直接寫在程式碼裡,提高安全性。
---
#### 🔹 2. 使用 twstock 取得台股資料
twstock 是 Python 的台股查詢套件,可獲取即時與歷史股價資訊。
#### 📌 基礎操作
```python=
import twstock
stock = twstock.Stock('2330') # 查詢台積電
print(stock.price) # 近期股價
print(stock.high) # 每天最高價
print(stock.low) # 每天最低價
print(stock.date) # 日期
```
🔹 應用場景:可用於股票走勢分析、技術指標計算。
---
#### 🔹 3. 使用 pandas 轉換股票數據
pandas 是 Python 的數據處理套件,可以將 twstock 取得的數據轉成 DataFrame 格式,方便處理。
#### 📌 DataFrame 結構
```python=
import pandas as pd
stock_data = {
'close': stock.close,
'date': stock.date,
'high': stock.high,
'low': stock.low,
'open': stock.open
}
df = pd.DataFrame.from_dict(stock_data)
print(df.head()) # 顯示前五筆數據
```
🔹 應用場景:DataFrame 讓我們可以輕鬆進行數據篩選、計算與視覺化。
---
#### 🔹 4. 使用 matplotlib 繪製股票走勢圖
matplotlib 是 Python 的視覺化工具,可用來繪製股價趨勢圖。
#### 📌 基本折線圖
```python=
import matplotlib.pyplot as plt
df.plot(x='date', y='close')
plt.title('2330 stock price')
plt.savefig('2330.png')
plt.close()
```
🔹 應用場景:視覺化股票價格變化,找出趨勢。
https://steam.oxxostudio.tw/category/python/example/matplotlib-index.html
---
#### 🔹 5. 使用 cloudinary 上傳圖片
因為 Line 傳輸圖片需要透過網址,但是在 python 這邊繪製的圖片沒有網址,所以需要上傳的雲端曲得網址後才可以使用 Line 傳輸。
cloudinary 允許我們將本地圖片上傳到雲端,並取得圖片網址。
#### 📌 設定 API
```python=
import cloudinary
import cloudinary.uploader
cloudinary.config(
cloud_name=os.getenv('CLOUDINARY_CLOUD_NAME'),
api_key=os.getenv('CLOUDINARY_API_KEY'),
api_secret=os.getenv('CLOUDINARY_API_SECRET')
)
```
#### 📌 上傳圖片並獲取網址
```python=
def upload_to_cloudinary(file_path) -> str:
"""Upload an image to Cloudinary and return its URL."""
try:
response = cloudinary.uploader.upload(file_path)
return response['secure_url']
except Exception as e:
print(f"Image upload failed: {e}")
return None
```
🔹 應用場景:上傳股票趨勢圖,讓 LINE Bot 能回傳圖片網址。
:::
1. 加上需要的 Python 套件
在 requirements.txt 加入
```=
numpy
matplotlib
twstock
pandas
cloudinary
```
2. 新增環境變數(在 render 新增三個環境變數)(上傳繪製圖片)
到 [Cloudinary](https://cloudinary.com/) 中申請 apikey
登入後點擊 Coding with APIs and SDKs

點擊右下角 View API Keys

記錄三個 APIKEY:Cloud name, API key, API secret
並在 Render 中儲存環境變數
```
CLOUDINARY_API_KEY
CLOUDINARY_API_SECRET
CLOUDINARY_CLOUD_NAME
```

3. 修改程式碼
新增`stock.py`檔案
:::spoiler `stock.py`
```python=
import os
from dotenv import load_dotenv
load_dotenv()
import twstock
import re
import cloudinary
import cloudinary.uploader
import matplotlib
matplotlib.use('Agg') # For server compatibility
import matplotlib.pyplot as plt
import pandas as pd
cloudinary.config(
cloud_name=os.getenv('CLOUDINARY_CLOUD_NAME'),
api_key=os.getenv('CLOUDINARY_API_KEY'),
api_secret=os.getenv('CLOUDINARY_API_SECRET')
)
def upload_to_cloudinary(file_path) -> str:
"""Upload an image to Cloudinary and return its URL."""
try:
response = cloudinary.uploader.upload(file_path)
return response['secure_url']
except Exception as e:
print(f"Image upload failed: {e}")
return None
def txt_to_img_url() -> str:
try:
sid = '2330'
stock = twstock.Stock(sid)
file_name = f'{sid}.png'
# Prepare stock data for plotting
stock_data = {
'close': stock.close,
'date': stock.date,
'high': stock.high,
'low': stock.low,
'open': stock.open
}
df = pd.DataFrame.from_dict(stock_data)
# Plot stock data
df.plot(x='date', y='close')
plt.title(f'{sid} stock price')
plt.savefig(file_name)
plt.close()
# Upload the image to Cloudinary
image_url = upload_to_cloudinary(file_name)
if image_url:
os.remove(file_name)
return image_url
else:
return None
except Exception as e:
print(f"Error generating stock trend chart: {e}")
return None
```
:::
在`app.py`中新增
```python=
from stock import txt_to_img_url
```
```python=
elif user_message == "台積電股票":
try:
image_url = txt_to_img_url()
if not image_url:
error_message = f"抱歉,沒有取得股票趨勢圖,{image_url}。"
line_bot_api.reply_message(
event.reply_token,
TextMessage(text=error_message)
)
return
line_bot_api.reply_message(
event.reply_token,
ImageSendMessage(
original_content_url=image_url,
preview_image_url=image_url
)
)
except Exception as e:
error_message = f"抱歉,無法生成股票趨勢圖,錯誤原因:{e}"
line_bot_api.reply_message(
event.reply_token,
TextMessage(text=error_message)
)
return
```
4. 將檔案推上 github

## 4. Gemini(AI 回覆訊息)
最後,將所有其他回覆交由 Gemini 處理
:::info
### 🤖 Gemini 回覆(Gemini API)
#### 🔹 1. 大語言模型(LLM)的基本概念
**大語言模型(LLM, Large Language Model)** 是基於深度學習的自然語言處理技術,能夠理解並生成類似人類的文字。
| 模型 | 提供者 | 特點 |
|------|------|------|
| **Gemini** | Google | 支援多模態(文字、圖片等),整合 Google 生態系 |
| **ChatGPT** | OpenAI | 通用 AI 對話助手,GPT-4 提供更強的推理能力 |
| **Claude** | Anthropic | 以安全性與對話長度優勢著稱 |
🔹 **應用場景**:Gemini 能夠回答問題、翻譯語言、生成文本等,非常適合用來提升 **LINE Bot 的互動性**。
---
### 🔹 2. 如何使用 Python 呼叫 Gemini API
Google 提供 `google.generativeai` 套件來簡化 API 呼叫。
#### 📌 安裝套件:
```bash
pip install google-generativeai
```
#### 📌 基本使用方式:
```python=
import google.generativeai as genai
import os
# 設定 API Key
genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
# 建立聊天模型
model = genai.GenerativeModel("gemini-pro")
# 發送請求
response = model.generate_content("介紹 Python 的特點")
print(response.text)
```
🔹 應用場景:Gemini API 可用於 智慧問答、內容生成,讓 LINE Bot 更有互動性。
:::
### 第一步:申請 Gemini API Key
1. 到 [Google AI Studio](https://aistudio.google.com/) 中申請

2. 點擊右上 Get API key
3. Create API key 並儲存
### 第二步:修改程式碼
1. 加上需要的 Python 套件
在 requirements.txt 加入 `google-generativeai
`
2. 新增環境變數(Gemini 的 APIKEY)
```
GEMINI_API_KEY
```

3. 修改 `app.py`
加入(Gemini 的模型名可能改變,如果無法執行可以看 Gemini 官網查看可使用的模型名稱)
```python=
import google.generativeai as genai
```
```python=
genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
model = genai.GenerativeModel("gemini-1.5-pro")
```
將此程式碼刪除
```python=
else:
reply_text = ("你說了:" + user_message)
```
修改成這樣
```python=
else:
response = model.generate_content(user_message) # 傳送使用者的問題給 Gemini
reply_text = response.text if response else "抱歉,我無法回答這個問題。"
```

:::success
## 📝 補充知識
### 🔹 1. Token 限制與計費方式
Gemini API 採用 Token 機制計費,每次請求會消耗一定數量的 Token,因此需要考量 API 的使用成本。
#### 📌 Token 概念:
- 1 Token 約等於 **1 個英文單字或 1 個標點符號**
- **輸入 + 輸出** 都會消耗 Token
- **免費方案**:每日有 Token 限制
- **付費方案**:可額外購買 Token,但需要留意計費方式
---
### 🔹 2. 如何讓 LINE Bot 依據使用者問題選擇合適的回應?
當 LINE Bot 需要與 Gemini API 結合時,應該根據使用者的輸入決定 **是否使用 Gemini** 或 **其他 API**。
#### 📌 指令解析(Command Parsing)
可以讓使用者透過 **關鍵字** 或 **標籤** 來觸發不同功能。
| 指令 | 解析 | Bot 行為 |
|------|------|---------|
| `@股票 2330` | 股票查詢 | 呼叫 **twstock API** 查詢台積電股價 |
| `@天氣 台北` | 天氣查詢 | 呼叫 **Weather API** 獲取台北天氣 |
| `@AI Python 是什麼?` | 問答模式 | 呼叫 **Gemini API** 產生回應 |
🔹 **應用場景**:
- LINE Bot 需要 **指令解析**,確保 API 調用符合使用者需求
- 如果是 **一般對話**,則讓 Gemini 回答
- 如果是 **特定指令**(如股票、天氣),則調用對應的 API
---
### 🔹 3. 多回應模式(文字 / 圖片 / 語音)與 Gemini API 的擴展應用
Gemini API 除了純文字回應,還可以擴展至 **圖片辨識、語音輸出** 等多模態應用。
#### 📌 可能的擴展應用:
- **圖片理解**:讓 Gemini 分析圖片內容,回傳描述
- **語音輸出**:結合 Google TTS(文字轉語音),讓 AI 以語音回覆
- **多輪對話**:記住使用者上下文,進行更自然的交流
#### 📌 LINE Bot 回應類型:
| 模式 | 內容 | 技術 |
|------|------|------|
| 文字回應 | AI 生成的回覆 | Gemini API |
| 圖片回應 | 股票趨勢圖、AI 生成圖片 | Matplotlib / Cloudinary |
| 語音回應 | 朗讀 AI 回覆 | Google TTS API |
🔹 **應用場景**:
- **股票查詢** 可以回傳 **趨勢圖**
- **天氣查詢** 可以提供 **語音播報**
- **一般問答** 以 **純文字** 回覆,但可選擇擴展到 **語音模式**
:::
---
## 恭喜!完成這次的課程了🎉
若是沒跟上,這裡有完整程式碼
https://github.com/xujk0217/GDG_linebot_test/tree/course2
---
回饋問卷
https://tally.so/r/3lzadB
---
### 下一堂課:
https://hackmd.io/@kHwE-_lVR8W5i9sJMWakSg/Hy0ctEIylx