在前幾個星期的練習中,我學會了如何呼叫 API 並讓程式自動抓取資料,但在實際開發時我們不可能每次都打開程式碼手動執行,所以今天要挑戰讓 API 的使用更方便。
我會練習做出兩種互動工具,CLI 是指透過終端機輸入指令與程式互動,GUI 則是以按鈕和文字框等視覺元件操作,例如使用 Python 的 Tkinter 或 Streamlit 建立介面。
這樣的做法不僅讓操作更容易,也能幫助我練習設計給使用者使用的介面,並理解使用者介面的基本概念。
今天我要做一個能讓使用者輸入城市名字並查詢天氣的天氣查詢工具。
因此,我有三個要完成主要目標。
第一,我會把取得天氣資料的程式包成一個獨立模組,這樣不管是命令列(CLI)工具還是圖形介面(GUI)都能共用這段程式碼。
第二,我會練習實作一個可以在終端機用指令操作的 CLI 工具。
第三,我練習做一個簡單的 GUI 版本,可以用 Python 的 Tkinter 或 Streamlit 來製作操作介面。這樣做不但方便使用,也能幫助我理解不同使用者介面的設計方法。
步驟 0:環境準備
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install requests python-dotenv
pip install streamlit
.env
OWM_API_KEY=我的OpenWeatherMap金鑰
CITY=Taipei
步驟 1:共用模組(封裝叫天氣 API 的程式)
這個檔案負責跟 OpenWeatherMap API 溝通,讓主程式只要呼叫 get_weather()
就能拿到結果。
import os
import requests
from dotenv import load_dotenv
from pathlib import Path
API = "https://api.openweathermap.org/data/2.5/weather"
class WeatherError(RuntimeError):
pass
def _load_api_key():
# 優先載入當前資料夾的 .env
load_dotenv(Path(__file__).with_name(".env"))
key = os.getenv("OWM_API_KEY")
if not key:
raise WeatherError("找不到 OWM_API_KEY(請在 .env 或環境變數設定)")
return key
def get_weather(city: str, units="metric", lang="zh_tw", timeout=10) -> dict:
"""
回傳精簡後的資料:
{
"name": "Taipei",
"temp": 29.3,
"feels_like": 31.1,
"humidity": 76,
"desc": "晴",
"raw": {... 原始 JSON ...}
}
"""
key = _load_api_key()
params = {"q": city, "appid": key, "units": units, "lang": lang}
try:
r = requests.get(API, params=params, timeout=timeout)
if r.status_code == 401:
raise WeatherError("401 未授權:API Key 無效或未啟用")
if r.status_code == 404:
raise WeatherError(f"找不到城市:{city}")
r.raise_for_status()
data = r.json()
except requests.exceptions.Timeout:
raise WeatherError("連線逾時,請稍後再試")
except requests.exceptions.ConnectionError:
raise WeatherError("連線錯誤,請檢查網路")
except requests.exceptions.RequestException as e:
raise WeatherError(f"HTTP 錯誤:{e}")
name = data.get("name") or city
main = data.get("main") or {}
weather0 = (data.get("weather") or [{}])[0]
return {
"name": name,
"temp": main.get("temp"),
"feels_like": main.get("feels_like"),
"humidity": main.get("humidity"),
"desc": weather0.get("description"),
"raw": data
}
步驟 2:做一個 CLI 指令
讓使用者能用指令列互動。
import argparse
import json
import os
from weather_client import get_weather, WeatherError
from dotenv import load_dotenv
from pathlib import Path
def main():
.env
取得預設城市 load_dotenv(Path(__file__).with_name(".env"))
default_city = os.getenv("CITY", "Taipei")
ap = argparse.ArgumentParser(description="查詢即時天氣(OpenWeatherMap)")
ap.add_argument("-c", "--city", default=default_city, help=f"城市(預設:{default_city})")
ap.add_argument("-u", "--units", choices=["metric", "imperial"], default="metric",
help="單位:metric=攝氏, imperial=華氏(預設 metric)")
ap.add_argument("-l", "--lang", default="zh_tw", help="語系(預設 zh_tw)")
ap.add_argument("--raw", action="store_true", help="輸出原始 JSON")
args = ap.parse_args()
try:
w = get_weather(args.city, units=args.units, lang=args.lang)
except WeatherError as e:
print("發生錯誤", e)
return
if args.raw:
print(json.dumps(w["raw"], ensure_ascii=False, indent=2))
else:
unit_symbol = "°C" if args.units == "metric" else "°F"
print(f" {w['name']} | {w['temp']}{unit_symbol}(體感 {w['feels_like']}{unit_symbol})"
f" | 濕度 {w['humidity']}% | {w['desc']}")
if __name__ == "__main__":
main()
我們想看執行結果,可以在終端機輸入:
python day26_cli.py
執行結果:
Taipei | 28.54°C(體感 33.49°C) | 濕度 80% | 多雲
如果想要指定城市,我們可以輸入:
python day26_cli.py -c Tokyo
執行結果:
Tokyo | 24.88°C(體感 25.09°C) | 濕度 64% | 陰,多雲
如果是想要將攝氏換成華氏,可以輸入:
python day26_cli.py -c "New York" -u imperial
執行結果:
New York | 67.57°F(體感 68.02°F) | 濕度 85% | 中雨
步驟 3:Tkinter GUI 版
這是一個桌面版小視窗,輸入城市後按下「查詢」,就會顯示當前天氣。
import tkinter as tk
from tkinter import messagebox
from weather_client import get_weather, WeatherError
def on_fetch():
city = entry.get().strip() or "Taipei"
btn.config(state="disabled")
try:
w = get_weather(city)
unit = "°C"
result = (f"{w['name']}\n"
f"溫度:{w['temp']}{unit}(體感 {w['feels_like']}{unit})\n"
f"濕度:{w['humidity']}%\n"
f"天氣:{w['desc']}")
lbl_result.config(text=result)
except WeatherError as e:
messagebox.showerror("錯誤", str(e))
finally:
btn.config(state="normal")
root = tk.Tk()
root.title("Day26 天氣查詢(Tkinter)")
建立主視窗物件並設定視窗標題。
frm = tk.Frame(root, padx=12, pady=12)
frm.pack()
建立一個框架元件作為內部排版的容器,設定內距,使用 pack() 放入主視窗。
tk.Label(frm, text="城市名稱:").grid(row=0, column=0, sticky="w")
entry = tk.Entry(frm, width=24)
entry.grid(row=0, column=1, padx=6)
entry.insert(0, "Taipei")
建立輸入框並放到同一列的下一欄,預設填入「Taipei」。
btn = tk.Button(frm, text="查詢", command=on_fetch)
btn.grid(row=0, column=2, padx=6)
lbl_result = tk.Label(frm, text="結果會顯示在這裡", justify="left", anchor="w")
lbl_result.grid(row=1, column=0, columnspan=3, pady=(12,0), sticky="w")
root.mainloop()
啟動 GUI 事件迴圈,視窗開始運作並等待使用者操作。
執行結果:
步驟 4:Streamlit GUI 版
這是網頁版界面,能用文字框輸入城市並即時顯示結果。
import streamlit as st
from weather_client import get_weather, WeatherError
st.set_page_config(page_title="Day26 天氣查詢", page_icon="Weather", layout="centered")
設定網頁標題、頁籤小圖示與版面配置;此設定需放在頁面最前面。
st.title("Day26 天氣查詢(Streamlit)")
city = st.text_input("城市", "Taipei")
units = st.radio("單位", options=["metric", "imperial"], format_func=lambda x: "攝氏" if x=="metric" else "華氏", horizontal=True)
if st.button("查詢"):
try:
w = get_weather(city, units=units)
unit = "°C" if units == "metric" else "°F"
st.success(f"{w['name']} 目前 {w['temp']}{unit}(體感 {w['feels_like']}{unit}),濕度 {w['humidity']}%,{w['desc']}")
col1, col2, col3 = st.columns(3)
col1.metric("溫度", f"{w['temp']}{unit}")
col2.metric("體感", f"{w['feels_like']}{unit}")
col3.metric("濕度", f"{w['humidity']}%")
with st.expander("原始回應 JSON"):
st.json(w["raw"])
提供可展開區塊以檢視完整 API JSON,方便除錯與了解結構。
except WeatherError as e:
st.error(str(e))
執行結果:
這是執行程式碼後跑出的網頁,可以選擇要查詢天氣的城市,接著再選擇攝氏溫度還是華氏溫度,最後在按查詢。
CLI 不顯示結果怎麼辦?
我們可以先檢查 .env
檔案裡的 API 金鑰是否正確,有時候因為金鑰錯誤或漏掉就無法成功取得資料。
401 Unauthorized 錯誤是為什麼?
這表示我們的 API 金鑰沒有被啟用,或者輸入錯誤。我們需要再次確認金鑰是不是打錯了,或是否已經過期。
GUI 介面卡頓該做什麼?
如果我們的應用程式使用同步方式呼叫 API,可能會造成畫面暫停不動。我們可以試著加入多線程或顯示等待提示,讓使用者知道程式正在工作中。
Streamlit 無法啟動需要做什麼?
需要確認我們有在對應的虛擬環境中安裝好 streamlit 套件,為避免環境問題,建議用 pip install streamlit
安裝。
今天我成功把 API 從只能用程式碼呼叫,變成了一個好操作的工具。我做出了一個 CLI 版本,可以在終端機用指令跟程式互動,這部分讓我練習了命令列的操作方式。同時,我也做了一個 GUI 版本,讓我體驗了視覺化介面設計與提升使用者體驗。這不只是學會怎麼呼叫 API,更重要的是學會如何讓別人也能輕鬆地使用我們的程式。接下來,我打算將這些介面和多個 API 整合起來,做成一個實用的生活資訊小工具,讓生活中的資料取得更方便。