Files
bot-rates/SUMMARY.md

5.8 KiB
Raw Blame History

開發過程摘要

記錄這個專案的演進脈絡、遇到的問題、以及為什麼做出這些選擇。

需求演進

階段 需求 產出
1 抓台銀 USD / JPY / EUR 即期賣出 Python CLI (bot_rates.py)
2 要給別人用、Windows、GUI、雙擊即跑、不要裝東西 Python tkinter + PyInstaller 方案
3 太麻煩,改用 Rust使用者無 Windows 機器 Rust + eframe + 交叉編譯
4 也給我一份 macOS 能跑的 BotRates.app bundle
5 壓縮 exe 方便傳送 bot_rates.zip5.4 MB → 2.7 MB
6 加上金額輸入 / 即時換算 三欄布局:幣別 / 輸入 / TWD
7 換算結果要能直接複製 「複製」按鈕 + 可選取文字

階段 1Python CLI

直接用 urllib 抓台銀 CSV起初用 csv.DictReader但踩到一個坑——CSV 有重複欄名(買入、賣出區段都用 匯率 / 現金 / 即期DictReader 只會保留最後一個同名欄位,所以找不到「本行即期賣出」。

解法:改用位置索引,即期賣出 在 col 13。

階段 2Python GUI最終未採用

計畫:tkinter + ttk 美化 + PyInstaller --onefile --windowed 打包成 exe。寫了 bot_rates_gui.pybuild.bat

問題:使用者手上沒有 Windows 機器PyInstaller 必須在目標 OS 上執行,無法從 macOS 直接產出 Windows exe。只能給使用者 build.bat 要他在 Windows 上自己打包——違背「不要裝東西」的初衷。

階段 3改用 Rust

Rust 可以從 macOS 乾淨地交叉編譯到 Windows單一靜態執行檔零 runtime 依賴,剛好解決 Python 方案的痛點。

技術選型

選擇 理由
eframe / egui Immediate mode GUI使用 OpenGLWindows 內建),不需要 GTK / Qt 這類系統依賴。交叉編譯幾乎沒坑。
ureq + rustls reqwest 預設用 native-tls 會綁 OpenSSL 或 Windows SChannel交叉編譯會有系統 library 問題。ureqrustls 是純 Rust無系統依賴。
x86_64-pc-windows-gnu 用 mingw-w64 當 linker-msvc 少了需要 Windows SDK 的麻煩。brew install mingw-w64 就好。

踩過的坑

LLVM 在 release 編譯時 SIGSEGV

error: rustc interrupted by SIGSEGV, printing backtrace
llvm::DeadArgumentEliminationPass::run

第一次 release 編譯時,Cargo.toml 用了 opt-level = "z" + lto = trueLLVM 的 DeadArgumentElimination pass 在處理 eframe 依賴時堆疊用光。

解法

  1. opt-level"z" 改成 3ltotrue 改成 "thin"(降低 pass 壓力)
  2. 加上 RUST_MIN_STACK=16777216(把執行緒堆疊從預設 2MB 拉到 16MB

兩個同時做下去就過了。

CJK 字型

egui 預設字型不含中文字,幣別名稱會顯示成「豆腐」。

解法:不 bundle 字型(會讓 exe 大 5-15MB改在執行期讀取作業系統字型。

  • WindowsC:\Windows\Fonts\msjh.ttc(微軟正黑體)
  • macOS/System/Library/Fonts/PingFang.ttc

fallback 鏈有多個候選msjhl / msyh / simhei找到第一個能讀的就用。

階段 4macOS .app bundle

cargo build --release 只產出裸執行檔,從 Finder 雙擊會跳 terminal 然後才開 GUI體驗差。

解法:手動組 .app bundle——其實就是特定目錄結構加一個 Info.plist

BotRates.app/
├── Contents/
│   ├── Info.plist           # 告訴 Finder 這是 app
│   └── MacOS/BotRates       # 實際執行檔

寫 plist 時標示 CFBundlePackageType = APPLCFBundleExecutable = BotRates 即可。另外跑 xattr -cr 清掉 quarantine 屬性避免 Gatekeeper 擋下。

階段 5壓縮

bot_rates.exe 5.4 MBzip -9 後 2.7 MB壓縮率 50%。PE 檔因為有大量重複的 LLVM / rustc 樣板程式碼壓縮效果很好。

階段 6金額換算

重新設計每一列為三欄 horizontal layout

[幣別+匯率 寬110]  [輸入 寬120]  [ → ]  [TWD 靠右]

狀態放在 App.amounts: HashMap<String, String>(幣別 → 輸入字串)。為什麼存字串而不是 f64?因為使用者輸入過程中可能有中間態(例如 "100."、空字串、"1,000"),存字串才能保留原樣。

輸入解析用 parse_amount,忽略逗號和空白再 parse。

TWD 顯示用 format_twd,手刻千分位 separator沒引入 num-format crate 以減少 binary size

階段 7複製功能

兩種複製方式並存:

  1. 按鈕複製(主要 UX按「複製」→ ui.output_mut(|o| o.copied_text = raw),複製純數字 3162.00(不含 NT$、不含逗號)。原因:這樣貼到 Excel 會被識別成數字而不是文字。
  2. 選取複製egui::Label::new(...).selectable(true),使用者可以用滑鼠拉選 TWD 文字後 Cmd/Ctrl+C

按下按鈕後短暫顯示「已複製」1.2 秒),用 Option<(String, Instant)> 追蹤狀態 + ctx.request_repaint_after 觸發回復重繪。

最終成果

項目 數值
Windows exe 大小 5.4 MBzip 後 2.7 MB
macOS 執行檔大小 5.2 MB
執行期依賴 無(兩平台皆零依賴)
首次冷啟動 < 1 秒
抓匯率耗時 網路 + 解析 < 500ms
總原始碼行數 ~310 行 Rust

如果要擴充

  • 加更多幣別:改 TARGETS 即可,其他地方都自動處理。
  • 換成即期買入 / 現金匯率:改 fetch_rates 裡的 col index3 買入 / 13 賣出 / 2 現金買入 / 12 現金賣出)。
  • 雙向換算TWD → 外幣):在每列加第二個輸入框,狀態多存一組字串;注意避免無限循環更新。
  • 歷史紀錄 / 圖表:台銀有 flcsv/1/month 等端點可以抓歷史,但就不是「即期」而是均價了。