import csv import io import threading import tkinter as tk import urllib.request from datetime import datetime from tkinter import ttk URL = "https://rate.bot.com.tw/xrt/flcsv/0/day" TARGETS = [ ("USD", "美元"), ("JPY", "日幣"), ("EUR", "歐元"), ] def fetch_rates(): req = urllib.request.Request(URL, headers={"User-Agent": "Mozilla/5.0"}) with urllib.request.urlopen(req, timeout=10) as resp: raw = resp.read().decode("utf-8-sig", errors="replace") reader = csv.reader(io.StringIO(raw)) next(reader, None) rates = {} wanted = {code for code, _ in TARGETS} for row in reader: if not row: continue code = row[0].strip() if code in wanted: rates[code] = float(row[13]) return rates class App(tk.Tk): def __init__(self): super().__init__() self.title("台灣銀行 即期賣出匯率") self.geometry("360x320") self.configure(bg="#f5f6fa") self.resizable(False, False) style = ttk.Style(self) try: style.theme_use("vista") except tk.TclError: style.theme_use("clam") style.configure("TFrame", background="#f5f6fa") style.configure("Title.TLabel", background="#f5f6fa", foreground="#1f2937", font=("Microsoft JhengHei UI", 14, "bold")) style.configure("Name.TLabel", background="#ffffff", foreground="#374151", font=("Microsoft JhengHei UI", 12)) style.configure("Rate.TLabel", background="#ffffff", foreground="#0f766e", font=("Consolas", 14, "bold")) style.configure("Hint.TLabel", background="#f5f6fa", foreground="#6b7280", font=("Microsoft JhengHei UI", 9)) style.configure("Accent.TButton", font=("Microsoft JhengHei UI", 10, "bold"), padding=6) container = ttk.Frame(self, padding=(18, 16)) container.pack(fill="both", expand=True) ttk.Label(container, text="台灣銀行 即期賣出", style="Title.TLabel").pack(anchor="w") ttk.Label(container, text="資料來源:rate.bot.com.tw", style="Hint.TLabel").pack(anchor="w", pady=(2, 12)) card = tk.Frame(container, bg="#ffffff", highlightthickness=1, highlightbackground="#e5e7eb") card.pack(fill="x") self.rate_labels = {} for i, (code, name) in enumerate(TARGETS): row = tk.Frame(card, bg="#ffffff") row.pack(fill="x", padx=14, pady=10) ttk.Label(row, text=f"{name} {code}", style="Name.TLabel").pack(side="left") lbl = ttk.Label(row, text="--", style="Rate.TLabel") lbl.pack(side="right") self.rate_labels[code] = lbl if i < len(TARGETS) - 1: tk.Frame(card, bg="#f1f2f6", height=1).pack(fill="x", padx=10) bottom = ttk.Frame(container) bottom.pack(fill="x", pady=(14, 0)) self.status = ttk.Label(bottom, text="載入中…", style="Hint.TLabel") self.status.pack(side="left") self.btn = ttk.Button(bottom, text="重新整理", style="Accent.TButton", command=self.refresh) self.btn.pack(side="right") self.after(100, self.refresh) def refresh(self): self.btn.state(["disabled"]) self.status.configure(text="載入中…", foreground="#6b7280") threading.Thread(target=self._worker, daemon=True).start() def _worker(self): try: rates = fetch_rates() self.after(0, self._on_success, rates) except Exception as e: self.after(0, self._on_error, e) def _on_success(self, rates): for code, _ in TARGETS: value = rates.get(code) text = f"{value:.4f}" if value is not None else "查無資料" self.rate_labels[code].configure(text=text) now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.status.configure(text=f"更新時間:{now}", foreground="#6b7280") self.btn.state(["!disabled"]) def _on_error(self, err): self.status.configure(text=f"載入失敗:{err}", foreground="#b91c1c") self.btn.state(["!disabled"]) if __name__ == "__main__": App().mainloop()