如果你讓批處理在夜間轉換一万份試算表,早晨發現其中三份回傳 False,布林儲存結果能給出的事後資訊就只有這些:失敗數量,却沒有檔案、工作表或十几種可能原因中的具體歸因。HotXLS 是 losLab 面向 Excel 檔案的原生 Delphi 和 C++Builder 元件,它用結構化診斷替代這個單一布林位。IXLSWorkbookProgress 接口提供 Diagnostics 列表和 OnDiagnostic 事件,為每次 Open、SaveAs 和 Recalculate 呼叫報告稳定的數值代碼、嚴重等級、失敗操作以及發生問題的工作表
為什麼布林儲存結果在大規模批處理中會失效?
布林結果造成的問題不是單個檔案失敗,而是一千個檔案失敗。當一万份檔案中有三份的 SaveAs 回傳非成功結果時,下一個問題總是相同的:這三份可以重試,還是需要人工處理?網絡共享上的權限錯誤,與計算引擎無法求值的公式不是同一類事件,悄悄超出格式限制的工作表也不同。只有成功或失敗結果時,它们都會變成完全相同的支援工單,工作人员不得不在 Excel 中逐個打開檔案並反複檢視,直到原因變得明顯。人工分流才是布林 API 的真正成本,而且會隨批處理規模線性增長,這正是錯誤處理最不希望看到的特性
深入了解 IXLSWorkbookProgress:TXLSDiagnostic 攜帶哪些資訊?
IXLSWorkbookProgress 是 HotXLS 用來報告操作進度和內部錯誤的接口,兩部分共用一個契約是有原因的:長時間執行的 Open、SaveAs 或 Recalculate 呼叫都需要在不中途抛出異常的情況下傳遞這些資訊。進度部分由 OnProgress 和 OnProgressEx 組成,觸發時提供阶段、狀態以及目前值/總值對。本文關註的是診斷部分:Diagnostics 屬性回傳 TXLSDiagnostics 列表,LastDiagnostic 快捷屬性指向最近一條記錄,OnDiagnostic 事件则在每條 TXLSDiagnostic 記錄創建立的瞬間觸發。每條記錄包含數值 Code、TXLSDiagnosticSeverity、產生它的 TXLSDiagnosticOperation、面向用户的 Message、SheetIndex 和 SheetName,以及保留底層回傳值的 NativeCode
var
Book: TXLSXWorkbook;
Diag: TXLSDiagnostic;
I: Integer;
begin
Book := TXLSXWorkbook.Create;
try
if Book.SaveAs('quarterly-report.xlsx') <> 1 then
for I := 0 to Book.Diagnostics.Count - 1 do
begin
Diag := Book.Diagnostics[I];
Writeln(Format('[%d] severity=%d sheet="%s": %s',
[Diag.Code, Ord(Diag.Severity), Diag.SheetName, Diag.Message]));
end;
finally
Book.Free;
end;
end;
像這样讀取 Diagnostics,本身就已經胜過布林結果,因為 Code 和 SheetName 會把谜团變成明確且可筛選的事實。TXLSDiagnostic 記錄包含的內容比示例打印的更多:RecordId 和 StreamOffset 可用於 BIFF 流中的位元組級取證,PartName 儲存產生問題的 OOXML ZIP 條目,例如 xl/worksheets/sheet3.xml。在圍绕這些字段建立工具前需要註意:目前版本的內置診斷呼叫點不會填充 RecordId 或 StreamOffset,因此它们都會維持構造函式預設值 -1,表示“不适用”而不是“零”。在處理程序中應將它们缺失視為正常情況,而不是錯誤
兩個引擎、同一套模型,以及一個不易察覺的差異
HotXLS 在同一報告模型後面提供兩個引擎:面向舊版 .xls 檔案的 BIFF8 外觀,以及面向 .xlsx 的 OOXML 外觀;兩者對 IXLSWorkbookProgress 的暴露方式並不完全相同。TXLSWorkbook 是 .xls 引擎,它正式實作了 IXLSWorkbookProgress,因此可以傳給任何需要該接口類型的位置。TXLSXWorkbook 是 .xlsx 引擎,它以相同名稱和類型暴露 Diagnostics、LastDiagnostic、OnDiagnostic、OnProgress 和 OnProgressEx 成员,但它是普通類,而不是該接口的正式實作,因此不能單獨滿足 IXLSWorkbookProgress 參數。實際開發中這通常影響不大,因為大多數代碼一次只针對一個具體活頁簿類,但這意味著不能編寫一個類型為 IXLSWorkbookProgress 的通用辅助方法,然後不加區分地傳入任一引擎的活頁簿對象。格式拆分直接帶來的唯一字段差異是 PartName:只有 XLSX 引擎會填充它,因為只有 OOXML 存在可命名的 ZIP 部件
什麼樣的診斷代碼才適合安全地進行分支判斷?
Code 字段是診斷中唯一值得硬編碼比较的部分;Message 不是,因為說明文字恰恰可能在後續版本中被改寫、重新翻譯或擴展,而不會被視為破坏性變更。HotXLS 的內置診斷代碼已經體現了這種區分:儲存相關代碼為 1000 到 1005,打開相關代碼為 1100 和 1101,計算相關代碼為 1200 和 1201,不支援格式代碼為 1300;每個區間內部留有間隔,而不是讓所有代碼連續排列。這样的間隔允許厂商在儲存阶段增加例如 1006 的新失敗模式,而不必重新编號現有 switch 語句依賴的代碼。在生產環境決定按代碼匹配前,任何診斷 API 都值得檢查這一點,而不只是 HotXLS。無论编號看起來多稳定,都應在自己的派發逻辑中保留預設分支,因為不斷演進的解析器或寫入器總會發現新的失敗模式。當需要進一步追查時,NativeCode 和 ExceptionClass 位於 Code 的下一層:NativeCode 保留底層回傳值,其中可能包括 Structured Storage 呼叫回傳的 HRESULT;ExceptionClass 記錄相關 Delphi 異常類型,通常足以發起精確的支援请求,而無需附加完整堆堆疊跟踪
嚴重等級和操作決定代碼的下一步行為
嚴重等級和操作會把診斷從日志行變成路由决策。TXLSDiagnosticSeverity 包含 Info、Warning、Error 和 Fatal,TXLSDiagnosticOperation 则為每條記錄標記產生它的呼叫:Open、Save、Calculate 或 Export。這兩個轴按設計相互獨立:xlsDiagnosticUnhandledException 是一個固定代碼,觸發時 Operation 會設定為實際引發異常的呼叫,因此 Code 回答“出了什麼問題”,而 Operation 單獨回答“發生在哪里”,無需為打開期間的異常和儲存期間的異常分別設計代碼。這種可組合性也讓路由變得机械化:記錄警告後繼續,例如透過 Aborted 標志取消儲存;統計錯誤並繼續批處理,例如工作表序列化失敗;遇到致命等級则停止批處理,因為這表示未處理異常已經展開呼叫,繼續執行可能基於半更新狀態工作。需要诚實說明的是:Info 作為新建立 TXLSDiagnostic 的預設枚举值存在,但目前 HotXLS 版本內置的診斷呼叫點只會產生 Warning、Error 或 Fatal;Info 是為未來保留的值,而不是引擎目前會發出的值
// same Diagnostics loop as above, routed by severity instead of printed flat:
for I := 0 to Book.Diagnostics.Count - 1 do
begin
Diag := Book.Diagnostics[I];
case Diag.Severity of
xlsDiagnosticWarning:
Writeln(Format('WARN [%d] %s', [Diag.Code, Diag.Message]));
xlsDiagnosticError:
begin
Writeln(Format('ERROR [%d] %s (sheet %s, native %d)',
[Diag.Code, Diag.Message, Diag.SheetName, Diag.NativeCode]));
Inc(FailedSheetCount);
end;
xlsDiagnosticFatal:
raise Exception.CreateFmt('Fatal HotXLS diagnostic %d: %s', [Diag.Code, Diag.Message]);
end;
end;
如何將 OnDiagnostic 接入批處理流程?
對於單個檔案,在每次呼叫後轮询 Diagnostics 可以工作;但回到一万份檔案的夜間批處理後,這種方式就失效了,因為每次 Open、SaveAs 和 Recalculate 呼叫開始時都會清空 Diagnostics。循環處理到第三個檔案後再讀取,你只能看到第三個檔案的診斷;前兩個檔案報告的內容已經消失。OnDiagnostic 透過把集合變成事件流解决了這個問題:在循環開始前订閱一次,同一個處理程序會按檔案顺序接收每條記錄,檔案名则透過實例字段維持在作用域內
type
TBatchConverter = class
private
FCurrentFile: string;
FFailedFiles: TStringList;
procedure HandleDiagnostic(Sender: TObject; Diagnostic: TXLSDiagnostic);
end;
procedure TBatchConverter.HandleDiagnostic(Sender: TObject; Diagnostic: TXLSDiagnostic);
begin
if Diagnostic.Severity >= xlsDiagnosticError then
FFailedFiles.Add(Format('%s: [%d] %s (sheet %s)',
[FCurrentFile, Diagnostic.Code, Diagnostic.Message, Diagnostic.SheetName]));
end;
// inside the batch loop:
Book.OnDiagnostic := HandleDiagnostic;
for I := 0 to FileNames.Count - 1 do
begin
FCurrentFile := FileNames[I];
if Book.Open(FCurrentFile) = 1 then
Book.SaveAs(ChangeFileExt(FCurrentFile, '.xlsx'));
end;
回呼實際會帶來多少開銷?
OnDiagnostic 從結構上看開銷很小:它只在問題已經發生時觸發,而問題數量相對於活頁簿包含的儲存格、行或工作表數量通常很少。相比之下,OnProgress 和 OnProgressEx 報告常規進度,因此從一開始就必須圍绕呼叫頻率設計。HotXLS 在 Open 和 SaveAs 期間每個工作表觸發一次工作表級進度,而不是每個儲存格或每行觸發一次;即使活頁簿包含數百万個儲存格,也能維持單次呼叫開銷较小。Recalculate 更進一步,會將自己的進度事件限制為依賴圖大約每推進百分之四觸發一次,因此完整重算會提供心跳,而不是用事件淹沒 UI 執行緒。診斷不需要任何這種節流,因為事件數量受實際問題數量限制,而不是受檔案大小限制
性能仍取决於你的一個地方,是處理程序內部。OnDiagnostic 會在執行 Open、SaveAs 或 Recalculate 的執行緒上同步觸發,因此會阻塞的處理程序——例如向遠程日志服務進行同步寫入——會成為該呼叫墙钟時間的一部分。單個檔案中這點通常察覺不到,但在一万份檔案的批處理中累积起來,就可能決定任務是夜間完成還是午餐時仍在執行。因此應緩衝處理程序需要做的工作並異步刷新,而不是在回呼內聯執行慢操作
結構化診斷在布林結果最薄弱的地方最有价值,也就是一次處理許多檔案而不是一個檔案的工作流。活頁簿審計和轉換流程是最清晰的例子:不要只為每個檔案記錄成功或失敗,而應將每個檔案的 Diagnostics 列表附加到審計記錄,報告就能告诉你失敗的原因,而不只是告诉你失敗了;這正是我们建立活頁簿審計與轉換工作台的文章試圖解决的核心問題。同样,進度和診斷也適合已經需要進度報告的工作流,這正是HotXLS 大型活頁簿性能指南覆盖的範圍:長時間執行的 Open 或 SaveAs 呼叫足夠常見,通常已經接入 OnProgress,此時旁邊增加 OnDiagnostic 几乎沒有额外成本
整個流程無需安装 Excel,也無需捕捉通用異常後猜測它代表什麼。IXLSWorkbookProgress 及其 Diagnostics、LastDiagnostic 和 OnDiagnostic 成员是面向 Delphi 和 C++Builder 的標準 HotXLS 元件,同時提供本文介绍的完整診斷代碼參考以及 Open、SaveAs 和 Recalculate API