5.8 KiB
開發過程摘要
記錄這個專案的演進脈絡、遇到的問題、以及為什麼做出這些選擇。
需求演進
| 階段 | 需求 | 產出 |
|---|---|---|
| 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.zip(5.4 MB → 2.7 MB) |
| 6 | 加上金額輸入 / 即時換算 | 三欄布局:幣別 / 輸入 / TWD |
| 7 | 換算結果要能直接複製 | 「複製」按鈕 + 可選取文字 |
階段 1:Python CLI
直接用 urllib 抓台銀 CSV,起初用 csv.DictReader,但踩到一個坑——CSV 有重複欄名(買入、賣出區段都用 匯率 / 現金 / 即期),DictReader 只會保留最後一個同名欄位,所以找不到「本行即期賣出」。
解法:改用位置索引,即期賣出 在 col 13。
階段 2:Python GUI(最終未採用)
計畫:tkinter + ttk 美化 + PyInstaller --onefile --windowed 打包成 exe。寫了 bot_rates_gui.py 和 build.bat。
問題:使用者手上沒有 Windows 機器,PyInstaller 必須在目標 OS 上執行,無法從 macOS 直接產出 Windows exe。只能給使用者 build.bat 要他在 Windows 上自己打包——違背「不要裝東西」的初衷。
階段 3:改用 Rust
Rust 可以從 macOS 乾淨地交叉編譯到 Windows,單一靜態執行檔,零 runtime 依賴,剛好解決 Python 方案的痛點。
技術選型
| 選擇 | 理由 |
|---|---|
eframe / egui |
Immediate mode GUI,使用 OpenGL(Windows 內建),不需要 GTK / Qt 這類系統依賴。交叉編譯幾乎沒坑。 |
ureq + rustls |
reqwest 預設用 native-tls 會綁 OpenSSL 或 Windows SChannel,交叉編譯會有系統 library 問題。ureq 搭 rustls 是純 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 = true,LLVM 的 DeadArgumentElimination pass 在處理 eframe 依賴時堆疊用光。
解法:
- 把
opt-level從"z"改成3、lto從true改成"thin"(降低 pass 壓力) - 加上
RUST_MIN_STACK=16777216(把執行緒堆疊從預設 2MB 拉到 16MB)
兩個同時做下去就過了。
CJK 字型
egui 預設字型不含中文字,幣別名稱會顯示成「豆腐」。
解法:不 bundle 字型(會讓 exe 大 5-15MB),改在執行期讀取作業系統字型。
- Windows:
C:\Windows\Fonts\msjh.ttc(微軟正黑體) - macOS:
/System/Library/Fonts/PingFang.ttc
fallback 鏈有多個候選(msjhl / msyh / simhei),找到第一個能讀的就用。
階段 4:macOS .app bundle
cargo build --release 只產出裸執行檔,從 Finder 雙擊會跳 terminal 然後才開 GUI,體驗差。
解法:手動組 .app bundle——其實就是特定目錄結構加一個 Info.plist:
BotRates.app/
├── Contents/
│ ├── Info.plist # 告訴 Finder 這是 app
│ └── MacOS/BotRates # 實際執行檔
寫 plist 時標示 CFBundlePackageType = APPL 和 CFBundleExecutable = BotRates 即可。另外跑 xattr -cr 清掉 quarantine 屬性避免 Gatekeeper 擋下。
階段 5:壓縮
bot_rates.exe 5.4 MB,zip -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:複製功能
兩種複製方式並存:
- 按鈕複製(主要 UX):按「複製」→
ui.output_mut(|o| o.copied_text = raw),複製純數字3162.00(不含NT$、不含逗號)。原因:這樣貼到 Excel 會被識別成數字而不是文字。 - 選取複製:
egui::Label::new(...).selectable(true),使用者可以用滑鼠拉選 TWD 文字後Cmd/Ctrl+C。
按下按鈕後短暫顯示「已複製」(1.2 秒),用 Option<(String, Instant)> 追蹤狀態 + ctx.request_repaint_after 觸發回復重繪。
最終成果
| 項目 | 數值 |
|---|---|
| Windows exe 大小 | 5.4 MB(zip 後 2.7 MB) |
| macOS 執行檔大小 | 5.2 MB |
| 執行期依賴 | 無(兩平台皆零依賴) |
| 首次冷啟動 | < 1 秒 |
| 抓匯率耗時 | 網路 + 解析 < 500ms |
| 總原始碼行數 | ~310 行 Rust |
如果要擴充
- 加更多幣別:改
TARGETS即可,其他地方都自動處理。 - 換成即期買入 / 現金匯率:改
fetch_rates裡的 col index(3 買入 / 13 賣出 / 2 現金買入 / 12 現金賣出)。 - 雙向換算(TWD → 外幣):在每列加第二個輸入框,狀態多存一組字串;注意避免無限循環更新。
- 歷史紀錄 / 圖表:台銀有
flcsv/1/month等端點可以抓歷史,但就不是「即期」而是均價了。