「這份活頁簿還定義著名叫 TaxRate 的名稱嗎?」這個問題,XLSX 套件裡的一小部分就能回答。工作表名稱、它們的隱藏與深度隱藏狀態、每一條活頁簿層級的已定義名稱,都住在 xl/workbook.xml(ECMA-376 Part 1, §18.2)裡,通常只有幾 KB。儲存格住在別處——每張工作表一個 xl/worksheets/sheetN.xml 部件,外加一份共用字串表,那裡才是幾乎所有位元組的所在。一個堅持在允許你問任何問題之前先建好完整活頁簿的載入器,會為資料夾裡的每一份檔案,把這一切都解壓、剖析一遍,只為回答開頭幾 KB 早已定案的問題
要緊的切分,在於讀活頁簿的形狀與讀它的重量之間。能在形狀之後停下的載入器,可以只為選定的工作表子集載入重量、稍後再補抓一張,把「開啟活頁簿」從全有全無的一步,變成每次呼叫各自劃定範圍的事。它也自帶兩個陷阱:一個關於索引基準,一個關於來源檔案得在原地待多久
僅中繼資料的 Open 讀什麼、停在哪裡
在 Open 之前把 TXLSXWorkbook.MetadataOnly 設為 True,HotXLS 會剖析活頁簿部件、樣式、佈景主題、已定義名稱、工作表名稱與可見狀態、連線、外部連結,以及 docProps/core.xml 與 docProps/app.xml 裡的文件屬性,然後跳過每張工作表的儲存格 XML 與每表衛星部件:註解、繪圖與表格。共用字串表也跳過——這比聽起來重要,在文字密集的活頁簿裡,那一個部件的大小就能與工作表們分庭抗禮
回來的活頁簿結構完整。Sheets.Count 是對的,每張工作表帶著真名與 Visibility,DefinedNames 已填充,ExternalLinks 列出每一個連結目標,Title、Author 與 LastModifiedBy 帶著檔案宣告的內容。只是每張工作表的儲存格都是零
這正是稽核要跑的資料。哪些活頁簿藏了工作表?哪些還定義著 TaxRate、定義成什麼?哪些連向去年就除役的檔案共用?下面的掃描器對單一檔案回答這三個問題。注意並排的兩種索引慣例:Sheets[] 從 1 起算、與 XLS 引擎一致,DefinedNames 與 ExternalLinks 則是普通的 0 起算清單
procedure ScanWorkbook(const FileName: WideString; Report: TStrings);
var
Workbook: TXLSXWorkbook;
Name: TXLSXDefinedName;
I: Integer;
begin
Workbook := TXLSXWorkbook.Create;
try
Workbook.MetadataOnly := True; // Open 時讀取,所以先設定
if Workbook.Open(FileName) <> 1 then
begin
Report.Add(FileName + ': open failed');
Exit;
end;
for I := 1 to Workbook.Sheets.Count do // Sheets[] 以 1 為基準
case Workbook.Sheets[I].Visibility of
xlsxSheetHidden:
Report.Add(FileName + ': hidden sheet ' + Workbook.Sheets[I].Name);
xlsxSheetVeryHidden:
Report.Add(FileName + ': very hidden sheet ' + Workbook.Sheets[I].Name);
end;
Name := Workbook.DefinedNames.FindByName('TaxRate');
if Name <> nil then
Report.Add(FileName + ': TaxRate = ' + Name.Formula);
for I := 0 to Workbook.ExternalLinks.Count - 1 do // 0 起算清單
Report.Add(FileName + ': links to ' + Workbook.ExternalLinks[I].Target);
finally
Workbook.Free;
end;
end;
掃描器刻意讀 Visibility 而不是布林值 Visible。兩種隱藏狀態下 Visible 都是 False,而深度隱藏的工作表才是稽核最想標記的那種——Excel 的取消隱藏對話框不列出它,只有 VBA 或直接改檔才請得回來
用 SelectedSheets 從四十張裡載入兩張
SelectedSheets 是活頁簿擁有的一個 TStringList。它至少含一個名稱時,Open 只為它點名的工作表剖析儲存格 XML;其餘每張工作表都帶著正確的名稱與可見性建立、不含儲存格。空清單——也是預設——載入所有工作表。名稱用清單自己的 IndexOf 查找,所以預設設定下比對不分大小寫,這正對上 Excel「同一活頁簿裡兩個工作表名稱不得只差大小寫」的規則
Workbook := TXLSXWorkbook.Create;
Workbook.SelectedSheets.Add('Summary');
Workbook.SelectedSheets.Add('Q3 Detail');
if Workbook.Open('consolidated-2026.xlsx') = 1 then
ShowSheet(Workbook.Sheets[1]); // ShowSheet 是你自己的 UI 程式碼
兩個開關只往一個方向互動。MetadataOnly = True 覆寫選擇、跳過所有工作表,所以兩個都設定的活頁簿回來時任何地方都沒有儲存格,選擇看起來壞掉了,其實只是被蓋過去。每次 Open 用一個就好:探索用僅中繼資料,鎖定目標的工作用選擇
ParallelParse 開著時,選擇性載入也貫穿平行剖析階段;未選的工作表在工作者工作裡被跳過,執行緒把時間花在你點名的工作表上。來源是二進位 .xlsb 活頁簿時,同樣兩個屬性照樣適用,但有一條範圍規則:帶 VBA 專案的 .xlsb 必須不帶這兩個選項開啟;任何一個設了,Open 會回報選擇性讀取不受支援
為什麼 LoadSheet(1) 載入錯的工作表?
因為 LoadSheet 吃 0 起算的位置,而 Sheets[] 從 1 起算。LoadSheet(1) 具現化的是活頁簿裡第二張工作表,而且載入一張已有儲存格的工作表是回傳 True 的無操作,所以弄錯的索引可能看起來成功,使用者點的那張卻還空著。這個轉換該放在你程式碼裡恰好一個地方
第二個陷阱是檔案壽命。LoadSheet 不在兩次呼叫之間把封存握在手裡。它按名稱重新開啟原始檔,剖析活頁簿部件、樣式與共用字串,然後剖析要求的工作表。信任那個檔案之前,HotXLS 會拿它的大小與最後寫入時間對照 Open 當下擷取的值;期間若有別的行程把活頁簿存掉了,LoadSheet 回傳 False,而不是把不同版本的儲存格混進你已持有的模型
procedure TReportForm.ShowSheetNo(SheetNo: Integer); // SheetNo 從 1 起算,同 Sheets[]
var
Sheet: TXLSXWorksheet;
begin
Sheet := FWorkbook.Sheets[SheetNo];
if Sheet.Cells.Count = 0 then
if not FWorkbook.LoadSheet(SheetNo - 1) then // LoadSheet 要的是 0 起算的位置
begin
StatusBar.SimpleText := 'The workbook changed on disk; reopen it';
Exit;
end;
Grid.ShowSheet(Sheet); // 你自己的表格程式碼
end;
這個設計帶來幾個後果。從 TStream 開啟的活頁簿沒有來源檔可回頭,所以串流開啟的 .xlsx 上 LoadSheet 回傳 False;加密活頁簿也一樣,它在剖析前就解密進記憶體。從串流選擇性開啟的二進位 .xlsb 是例外,因為 HotXLS 會為延後的工作表暫存一份串流複本。而真正空的工作表每次呼叫都會重讀,因為零儲存格也正是被跳過工作表的樣子
成本模型決定用法。每次 LoadSheet 都重新剖析共用字串表,逐張具現化十張工作表,那一部分就付十次。對使用者偶爾多開一個分頁的檢視器,這個價格合理。已經知道工作表集合時,改用 SelectedSheets 重開:一趟封存、一次共用字串剖析。內容上也有差別:LoadSheet 帶來的是工作表的儲存格、值、公式與格式,而註解、繪圖與表格隨 Open 一起來,所以程式碼要讀註解或表格的工作表,一開始就該放進選擇
為什麼一次往返會改動你從未碰過的部件?
全部重新產生的儲存,會重寫從沒變過的位元組。解壓佈景主題、建物件模型、序列化、再壓縮,結果與原始不同,即使沒有人動過佈景主題。對 VBA 專案與樞紐分析快取,代價比一份吵鬧的差異更高:VBA 專案是帶自己內部串流的二進位複合檔,樞紐分析快取握有來源資料的快照,而樞紐分析表定義按位置定址其欄位——沒人改動它們時,最安全的做法就是別碰它們的位元組
預設為 True 的 PreserveRawParts 讓 SaveAs 直接從來源封存複製未變更部件的壓縮位元組。涵蓋範圍:佈景主題、VBA 專案、連線、圖表、內嵌媒體、印表機設定、樞紐分析表與樞紐分析快取的定義與記錄——每項都只在模型說它未被修改時。這些部件在途中從不被解壓或重新壓縮。把屬性設成 False 會強制每個部件完整重新序列化——想讓輸出只反映物件模型內容時,伸手拿的就是這個開關
原樣複製路徑用的是與 LoadSheet 相同的檔案檢查:來源必須還在、大小與時間戳與 Open 當下相符。不符時,SaveAs 照常序列化那些部件,檔案仍然完整。兩種情況一定走序列化路徑:來源是二進位 .xlsb 的活頁簿——讀取時已轉成 XML——以及設了 StrictOOXML 的儲存,因為部件命名空間會變
儲存一份部分載入的活頁簿需要一個決定。對 .xlsb 來源,還有任何工作表延後時 SaveAs 會拒絕,並在 Diagnostics 記下「Load every deferred binary worksheet before saving」。對 .xlsx,把選擇性載入的活頁簿當唯讀檢視:空殼工作表沒有儲存格可讓寫入器發出,所以編輯並儲存之前,先為每個空殼呼叫 LoadSheet,或不帶選擇重開
刻意重開的探索管線
站得住的模式有兩趟。第一趟以僅中繼資料開啟每個檔案並做決定。第二趟只重開命中的檔案、帶工作表選擇、做工作。重開看似浪費,直到你算出第一趟省下了什麼:第二次 Open 是唯一剖析儲存格的一次,而且只剖析第一趟選中的工作表
procedure ProcessInvoiceFile(const FileName: WideString);
var
Probe, Workbook: TXLSXWorkbook;
Wanted: TStringList;
I: Integer;
begin
Wanted := TStringList.Create;
try
// 第一趟:探索,不碰儲存格
Probe := TXLSXWorkbook.Create;
try
Probe.MetadataOnly := True;
if Probe.Open(FileName) <> 1 then
Exit;
for I := 1 to Probe.Sheets.Count do
if Pos('Invoice', Probe.Sheets[I].Name) > 0 then
Wanted.Add(Probe.Sheets[I].Name);
finally
Probe.Free;
end;
if Wanted.Count = 0 then
Exit; // 這份檔案裡沒有值得剖析的東西
// 第二趟:只為選中的工作表剖析儲存格
Workbook := TXLSXWorkbook.Create;
try
Workbook.SelectedSheets.Assign(Wanted);
if Workbook.Open(FileName) = 1 then
PostInvoices(Workbook); // 你自己的處理程式碼
finally
Workbook.Free;
end;
finally
Wanted.Free;
end;
end;
在 Open 之後清掉 MetadataOnly,不會把探測器變成完整活頁簿。旗標在開啟時讀取;事後改它只影響該物件的下一次 Open。探測器仍可以透過 LoadSheet 一張一張長出工作表,一兩張這樣做還算合理,但管線更適合乾淨的重開——它還會把每張工作表的註解與表格一起帶來
它與其他大型檔案槓桿的搭配
選擇性載入削減的是你剖析的東西。工作表一旦載入後的成本它管不著,工作需要每張工作表的每個儲存格時它也幫不上。那種情況關於儲存,見 列塊儲存格儲存與範圍樣式 一文;也關於剖析吞吐量,見 平行 XLSX 剖析與記憶體配置器。任務是純讀取、完全沒有編輯時,串流直接讀取器 整個跳過活頁簿物件模型,比任何會建一個的載入策略都更合適
這一切都在你自己的 Delphi 或 C++Builder 執行檔裡跑,做掃描的伺服器上不必安裝 Excel。支援的格式、版本與授權列於 HotXLS Delphi 試算表元件頁面