本篇目的:讀者可以了解如何管理系統設定與使用者設定,並且可學會使用 json 模組進行讀取與寫入
本篇專欄的目的是讓製造業產線工程師的大家,可以做出符合自身需求資料整理輸出 GUI 程式,而我們作分析一定需要2種資料:產出資料與 SPEC。SPEC通常會由使用者在 UI 中進行輸入,而本篇專欄即將進入第四部分 ”使用者輸入資料的管理”,預計會提到以下內容:
如何儲存常用參數與使用者設定資料 (day 17 ~ day 18)
如何在使用者輸入與讀取 UI 資料時,做資料格式的驗證 (day 21~22)
如何優化輸入介面,讓它有複製貼上、標示超規數據等方便功能 (day 19~20)
我們在 day 9~13 有提到如何讀取 UI 元件內的使用者輸入內容,但使用者們一定會希望程式能儲存他們的輸入,例如我們一定希望同樣的產品 SPEC 只要設定一次、就能產生多次結果,又或是給產線使用的程式一定是讓工程師設定並儲存、然後讓產線人員在執行時選參數。
今天 day 17 便會說明如何儲存這些使用者輸入資料,我們會選用 Python 內建的 json 模組處理這些事情,讓我們的 GUI 程式擁有長久的記憶,解放我們的工時。
Json 是一種廣泛使用的資料儲存格式,許多網路資料都適用此格式儲存,例如證交所的服務API、政府資料平台等。我們選用此格式儲存設定的原因如下:
結構上與 dict 非常像,且 Python 有原生的 json 模組可處理
檔案小
人容易閱讀
Json 檔案打開來的格式如下。其中最外層的 {} 大括號包覆整個資料,資料內有屬性與值,資料之間用逗號隔開。資料內用雙引號 ”” 包覆起來的文字被稱為屬性,而屬性後面的對應值可以是字串、數字、陣列、字典、布林值、空值等。因此 json 可以作成巢狀結構,如下面的 data 內有多班公車資訊組成之 list。
Python 讀取時會將整個資料讀取成一個 dict。其中的屬性一定會被讀取成字串,而對應值的資料形式則由資料本身決定 (例如下面資料的 size,對應值是整數 0,但 "年度[民國年]" 的對應值會是字串而不是整數)。
{
"contentType": null,
"isImage": false,
"size": 0,
"data": [
{
"路線名稱": "綠幹線",
"起迄站及行經重要站點": "臺南-新化-玉井",
"路線類別": "幹支線",
"業者": "興南",
"年度[民國年]": "114",
"統計": "1,075,948"
},
{
"路線名稱": "綠1",
"起迄站及行經重要站點": "新化-臺鐵南科-臺鐵善化",
"路線類別": "幹支線",
"業者": "興南",
"年度[民國年]": "114",
"統計": "44,498"
},
]
}
程式的設定檔大致上可分為儲存初始設定的設定檔,與在使用過程中儲存使用者輸入的設定檔。這兩種設定檔在存放位置、用途都有不同差別。以下會針對這兩個設定檔作介紹
系統初始設定檔適合放不容易變動、或是在使用者第一次使用時載入的資料,例如下拉清單的選項、預設 SPEC 與路徑、讀取的欄位清單等。另外通常只會有一個 json 檔案,名稱常叫做 config.json。
我們通常會手動編寫 json 並編輯這些資料,儲存於與 main.py 相同的路徑中,方便之後打包成 exe 時可以直接將這個檔案一起包進去。
另外當我們執行專案打包的 exe 檔後,打包進去的外掛資源(如 config.json、圖示)在執行時會被解壓縮到系統的臨時目錄 sys._MEIPASS 中;而我們在開發時是用 cmd 執行,這些設定檔通常會自己放在與 main.py 相同的路徑。
為了讓程式在「開發期(執行 .py)」與「打包後(執行 .exe)」都能無縫找到檔案,我們可以編寫一個 get_resource_path 函式:
import sys
from pathlib import Path
def get_resource_path(relative_path: str | Path) -> Path:
"""
取得資源檔案的絕對路徑
相容於:
1. 開發階段:直接執行 .py 檔 (以當前腳本所在目錄為準)
2. 打包階段:PyInstaller 打包後的單一 exe 檔 (以 sys._MEIPASS 暫存目錄為準)
"""
if hasattr(sys, '_MEIPASS'):
# 打包成 exe 後,PyInstaller 解壓縮檔案的暫存資料夾
base_path = Path(sys._MEIPASS)
else:
# 開發階段:當前 py 檔案所在的資料夾
base_path = Path(__file__).resolve().parent
return base_path / relative_path
# 使用範例:取得 config.json 的正確路徑
config_path = get_resource_path("config.json")
注意:此 function 必須放在
main.py執行,否則開發階段取得的路徑會異常
使用者的設定檔可以有很多個,例如輸出檔案格式的 SPEC 以及計算良率的 SPEC 可以分成兩個檔案,檔名為自己設定。
通常會設定使用者第一次開啟程式時,生成這些 json 檔案 (裡面是空的)。然後在使用者輸入資訊並按下存檔後寫入資訊到 json 檔案中。
在 Windows 系統中,這些使用者設定檔習慣儲存在 C:\Users\使用者名稱\AppData\Roaming\應用程式名稱 內,主要原因如下:
具備完整讀寫權限:AppData 是使用者本機專屬目錄,一定可以寫入檔案
升級軟體不遺失設定:未來就算重新發布 exe 檔,使用者的參數設定依然在存在該路徑中
在 Python 中,我們可以透過環境變數 APPDATA 取得該路徑,並搭配 mkdir 自動建立專屬資料夾:
import os
from pathlib import Path
# 專案名稱,也是程式本身的名稱
PROJECT_NAME = 'MyWaferTool'
# 透過 Windows 環境變數取得 AppData 路徑
app_data_dir = Path(os.environ["APPDATA"]) / PROJECT_NAME
# 自動建立資料夾 (parents=True 會遞迴建立上層資料夾,exist_ok=True 避免重複建立時報錯)
app_data_dir.mkdir(parents=True, exist_ok=True)
# 使用者設定檔完整路徑
user_setting_path = app_data_dir / "user_spec.json"
我們另外設定一個 config.json 存放一些初始值的目的是避免 controller 等模組充滿了各種預設常數(例如多組讀檔/輸出路徑),造成編寫上的混亂。我們可以用以下方式判斷這個常數應該寫在 py 模組裡還是 config.json 中:
應該寫在 py 模組裡:與程式執行流程、邏輯相關的項目,若有錯誤會導致 error 或是預期外的執行結果。例如前面提到的使用者 SPEC 設定檔的名稱、晶粒座標對應的 X/Y 欄位名稱、Qt 的 signal 、程式本身的名稱等
應該寫在 config.json 裡:需要在程式開啟時載入,但會讓使用者選擇或修改、與程式執行流程較不相關的項目,例如下拉式選單的選項、初始的讀檔/輸出路徑、初始 SPEC 等
.json 檔案大家應該會覺得前面蹲了這麼久的馬步,怎麼到這裡才說明 json 檔案如何處理。原因是因為 json 的模組使用上很簡單,跟讀寫 txt 非常類似,反而檔案路徑與存儲資料的的管理才是”使用者配置設定”的重點。
# file_path 是 json 的檔案路徑
if file_path.exists(): # 檢查檔案是否存在
# 使用 open 開啟檔案,可以在讀取後自動關閉檔案
# 讀取使用模式'r'
# 編碼通常是 utf-8
with open(file_path, 'r', encoding='utf-8') as f:
self.sys_config = json.load(f) # 讀取 json 檔案
# 寫入使用模式'w'
# 寫入編碼一樣是 utf-8
# indent: 縮排數,讓檔案比較好閱讀,如上面的公車範例
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
今天我們了解了資料的分類、路徑的檢查方式、以及 json 檔案的讀寫。但如同 day 15 提到的,我們不該讓 json 的讀取寫入散佈在 controller 之中,而是應該用一個 class 去管理它。
我們在 day 14 中知道了 class 的建構方式,但那個 class 是關於 data pipline 的建構。而在明天 day 18 的文章中,我會說明如何實作一個 ”完整,可重複使用” 之功能模組,並且是從 0 開始定義架構。若我們能掌握定義架構的方法,無論之後是自己寫或是 vibe coding,都能產出符合自身期待的模組。