先把匯出流程分成準備 寫入與發布
匯出失敗時只顯示『寫檔失敗』,操作員不知道是路徑不存在、沒有權限、磁碟已滿還是內容編碼不合法。應把流程分成解析查詢、建立暫存檔、寫入資料、驗證 rows 與 encoding、最後發布。每階段使用不同錯誤類別,避免重試錯方向。
notfound 表示目標路徑或父目錄不存在;accessdenied 表示目前身分無法讀寫;diskfull 表示空間或配額不足;encodingbad 表示資料無法依指定編碼序列化。實際作業系統可能回傳更細的錯誤碼,應保留原始例外與路徑摘要,再映射成這些產品層原因。
existingfile 是另一種策略結果,不應直接 overwrite 或 delete。若同名檔已存在,先依匯出契約選擇拒絕、產生帶 request_id 的新名稱,或走明確版本化發布;不能因使用者按一次按鈕就刪除可能仍在使用的檔案。
本文只討論通用檔案流程,不假設 Q 系列 PLC、HMI 或通訊模組提供任何特定檔案 API。權限、網路磁碟、檔案鎖、編碼與檔名限制都要以目標作業系統和部署文件確認。
錯誤分類要保留可排查證據
notfound 的正常排查是確認父目錄、掛載點與拼寫;accessdenied 要確認執行身分、ACL、唯讀屬性與檔案鎖;diskfull 要查剩餘空間、配額與暫存檔清理;encodingbad 要保存欄位、錯誤位置與指定編碼。四者都不應統一重試,因為重試不會自行增加權限或修正壞字元。
資料本身也可能失敗:查詢回傳零列可以是合法空結果,不能和 queryfail 混用。manifest 應標記 empty_success 或 query_failed;若查詢成功但資料列有編碼錯誤,則在 publish 前停止,保留錯誤欄位與列號,不發布半份檔案。
正常案例是輸出目錄存在、暫存檔完成、rows=120、UTF-8 驗證通過,最後發佈成正式檔。另一個失敗案例明訂輸出ASCII,rows=120但第87列含中文,無法依ASCII編碼,結果為 encodingbad、正式檔不變、暫存檔依政策隔離供排查。不要把第 1 至 86 列發布後再補第 87 列,否則使用者會拿到不完整資料。
日誌至少帶 request_id、目標目錄摘要、檔名、錯誤分類、原始錯誤代碼、rows、編碼、開始與結束時間;路徑若包含秘密或個資,應遮罩。排查順序先看階段,再看原始原因,最後才調整重試或清理策略。
錯誤回報也要說明是否可安全重試。notfound 在建立目錄或修正設定後才可重試;accessdenied 要由授權者調整身分或 ACL;diskfull 需清理或增加配額;encodingbad 要修正資料或編碼契約。沒有狀態改變時連續重試只會製造更多日誌與暫存檔。
查詢階段若已失敗,不應建立看似完整的空檔;若查詢成功且合法零列,則可產生帶欄位標頭的空報表,但 manifest 必須寫 success_empty。這兩者對下游的意義不同,前者要求排查資料源,後者可能只是時間範圍沒有資料。
同一檔案系統內的暫存與原子發布
建議暫存檔建立在正式檔同一目錄,完成寫入、flush、關閉與驗證後,再以同一檔案系統支援的原子 rename/replace 發布。只有契約已允許取代現有版本時才使用replace,不能用它實作拒絕覆寫政策。這能讓讀者看到舊檔或完整新檔,降低讀到半份內容的機會;跨檔案系統、網路掛載或特殊檔案系統不能自行假定同樣語意。
發布前驗證 rows、欄位、編碼、檔案大小、預期 schema 與必要 hash。驗證的是暫存檔實際內容,不是記憶體中預期值。任一項失敗就保留舊正式檔,暫存檔依保留政策加上 request_id 隔離,不把錯誤資料改名成正式檔。
existingfile 發生時,不要先刪除再寫入,因為中途失敗會讓使用者失去最後一份可讀檔。可以拒絕並回報 AlreadyExists,或在規格允許時使用唯一版本檔名,再由明確的發布索引切換目前版本。切換索引本身也要有驗證與回復策略。
若政策為同名即拒絕,不能先exists檢查再replace,因為檢查後可能有人建立同名檔。應採平台支援的原子不覆寫發布,或使用獨占建立的唯一版本名稱與受控索引;os.replace會取代既有檔案,只有明確允許覆寫時才適用。
若流程在 rename 前崩潰,正式檔仍應維持舊版,重啟時掃描同目錄的暫存檔與 manifest,依 request_id 判斷可恢復、可重驗證或應隔離。不要看到同名暫存檔就直接續寫,也不要把未知狀態宣稱匯出成功。
同一目錄不代表所有平台都提供完全相同的可見性;仍要查文件與實際掛載型態。發布後可重新開檔讀取並核對 hash,必要時再更新索引。若讀者在發布瞬間仍看到舊版,應依讀取端快取政策處理,不能把短暫可見延遲誤判成檔案半寫。
暫存檔名稱應包含 request_id 或隨機識別,但不把使用者秘密直接放入檔名。清理任務只刪除已確認失敗、超過保留期的暫存檔,保留未知狀態供人工復核。這樣重啟恢復與磁碟空間政策不會互相破壞。
驗收與適用限制
若使用者選擇覆寫語意,仍要先驗證完整輸出並以明確版本或備份保留舊檔;不能把 overwrite 當成 delete 加新檔的兩步操作。任何失敗都要能回到可讀的舊版本。
匯出完成通知應在正式檔發布並重新驗證後才送出;暫存檔寫完或查詢完成都不能提前通知成功。
驗證 rows 時還要檢查欄位名稱與順序,因為列數正確不表示 schema 正確。若欄位少一個但仍能開啟檔案,必須在 publish 前拒絕並保存 manifest_invalid 原因。
磁碟滿與權限不足可能同時出現,分類應以最先被可靠觀測的階段為主,並保留原始錯誤鏈。清理暫存檔前要確認沒有其他 request 正在寫入,避免為了釋放空間刪掉可恢復的未知工作。
離線驗收包括:父目錄不存在、權限撤銷、磁碟配額不足、指定編碼不能表示某字元、合法零列、查詢失敗、existingfile、寫入中斷、驗證 rows 不符與 rename 失敗。每項都確認正式檔是否仍可讀、錯誤分類是否正確、manifest 是否留下證據。
限制是原子 rename 的保證取決於同一檔案系統與作業系統文件,網路檔案服務可能有快取、鎖與不同語意;flush 也不等於所有硬體故障都不會遺失資料。部署前要在目標環境做故障注入。
常見問題與官方參考
FAQ1:匯出成零列就是失敗嗎?答:不一定;合法查詢空結果應標記 empty_success,查詢執行失敗才是 query_failed。
FAQ2:existingfile 可以先刪再重建嗎?答:不建議;應拒絕、版本化或經明確發布流程切換,保留舊正式檔。
FAQ3:暫存檔放任意目錄也能原子改名嗎?答:不能假定;同一檔案系統是必要前提之一,跨掛載要查官方文件。
FAQ4:寫到一半失敗可以發布已完成的列嗎?答:不可以,除非規格明確定義分片;一般匯出應驗證完整 rows 後才發布。
參考:Python os.replace 官方文件:檔案取代與平台語意參考;實際原子性與跨檔案系統限制仍須查目標作業系統。
參考:Python tempfile 官方文件:暫存檔與暫存目錄建立參考,不代表部署環境一定具備相同檔案保證。