131 lines
4.5 KiB
Python
131 lines
4.5 KiB
Python
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()
|