iT邦幫忙

2025 iThome 鐵人賽

DAY 26
0
Modern Web

每天一點 API:打造我的生活小工具系列 第 26

Day 26 — 天氣查詢工具實作:從命令列到圖形介面

  • 分享至 

  • xImage
  •  

在前幾個星期的練習中,我學會了如何呼叫 API 並讓程式自動抓取資料,但在實際開發時我們不可能每次都打開程式碼手動執行,所以今天要挑戰讓 API 的使用更方便。

我會練習做出兩種互動工具,CLI 是指透過終端機輸入指令與程式互動,GUI 則是以按鈕和文字框等視覺元件操作,例如使用 Python 的 Tkinter 或 Streamlit 建立介面。

這樣的做法不僅讓操作更容易,也能幫助我練習設計給使用者使用的介面,並理解使用者介面的基本概念。

今天的小專案目標

今天我要做一個能讓使用者輸入城市名字並查詢天氣的天氣查詢工具。

因此,我有三個要完成主要目標。

第一,我會把取得天氣資料的程式包成一個獨立模組,這樣不管是命令列(CLI)工具還是圖形介面(GUI)都能共用這段程式碼。

第二,我會練習實作一個可以在終端機用指令操作的 CLI 工具。

第三,我練習做一個簡單的 GUI 版本,可以用 Python 的 Tkinter 或 Streamlit 來製作操作介面。這樣做不但方便使用,也能幫助我理解不同使用者介面的設計方法。

實作流程

步驟 0:環境準備

  • 建立並啟用虛擬環境(Windows PowerShell)
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
  • 讀取 API 金鑰
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 事件迴圈,視窗開始運作並等待使用者操作。

執行結果:
https://ithelp.ithome.com.tw/upload/images/20251008/20178708Z98N1gtJD7.png
https://ithelp.ithome.com.tw/upload/images/20251008/20178708FFRJ2JMZtm.png

步驟 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)
  • 查詢按鈕、呼叫 API 與主結果
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))

執行結果:
https://ithelp.ithome.com.tw/upload/images/20251008/20178708Zeb2Pbrmza.png
這是執行程式碼後跑出的網頁,可以選擇要查詢天氣的城市,接著再選擇攝氏溫度還是華氏溫度,最後在按查詢。
https://ithelp.ithome.com.tw/upload/images/20251008/20178708Df0jtKHkpT.png
https://ithelp.ithome.com.tw/upload/images/20251008/20178708kYLU6GRdsz.png

常見問題與解決方法

CLI 不顯示結果怎麼辦?
我們可以先檢查 .env 檔案裡的 API 金鑰是否正確,有時候因為金鑰錯誤或漏掉就無法成功取得資料。

401 Unauthorized 錯誤是為什麼?
這表示我們的 API 金鑰沒有被啟用,或者輸入錯誤。我們需要再次確認金鑰是不是打錯了,或是否已經過期。

GUI 介面卡頓該做什麼?
如果我們的應用程式使用同步方式呼叫 API,可能會造成畫面暫停不動。我們可以試著加入多線程或顯示等待提示,讓使用者知道程式正在工作中。

Streamlit 無法啟動需要做什麼?
需要確認我們有在對應的虛擬環境中安裝好 streamlit 套件,為避免環境問題,建議用 pip install streamlit 安裝。

今日總結

今天我成功把 API 從只能用程式碼呼叫,變成了一個好操作的工具。我做出了一個 CLI 版本,可以在終端機用指令跟程式互動,這部分讓我練習了命令列的操作方式。同時,我也做了一個 GUI 版本,讓我體驗了視覺化介面設計與提升使用者體驗。這不只是學會怎麼呼叫 API,更重要的是學會如何讓別人也能輕鬆地使用我們的程式。接下來,我打算將這些介面和多個 API 整合起來,做成一個實用的生活資訊小工具,讓生活中的資料取得更方便。


上一篇
Day 25 — API 自動化:每天自動更新資料
系列文
每天一點 API:打造我的生活小工具26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言