技術文章

用 Delphi 與 HotXLS GetSheetNames 快速列出工作表名稱

有時候一道上傳常式唯一需要回答的問題是結構性的:這份活頁簿有沒有一個叫做「Mapping」的工作表,或它帶了幾個分頁。用呼叫 Open 來回答這件事,是昂貴的做法。一次完整開啟會展開共用字串表、解碼每一筆樣式記錄,並走過每個工作表的儲存格,因為它無從知道你只想要目錄。在一份大型檔案上,那是數百 MB 的配置與好幾秒的 CPU,只為了讀一份只佔幾 KB 的清單。HotXLS,losLab 的原生 Delphi 試算表程式庫,單獨把那份清單給你:GetSheetNames 交回工作表名稱,按活頁簿順序,而不具現化任何一個儲存格

為什麼目錄讀起來很便宜

兩種試算表格式都把它們的目錄放在接近前面的位置,這正是讓一次列舉呼叫快速而非只是聰明的原因。一個 OOXML 封包把工作表目錄放在 xl/workbook.xml 裡,這個部分無論活頁簿持有十列或一千萬列都保持很小。BIFF8 .xls 則把它的 BoundSheet 記錄儲存在活頁簿全域串流的開頭,在任何儲存格資料之前。所以一次列舉呼叫所避開的工作,相對於一次完整開啟並非四捨五入的誤差。它是檔案的大部分。讀取目錄無論列數為何都花同樣的少數幾 KB,而一次完整開啟則隨資料擴展,在一份數 MB 的活頁簿上,那個差距在觸碰的位元組與配置的記憶體兩方面都達到好幾個數量級

HotXLS 在 Delphi 的 GetSheetNames 只讀 XLSX 或 XLS 檔的工作表目錄,完整開啟則走訪每個儲存格
目錄躺在 workbook.xml 或 BoundSheet 記錄裡,列出只花幾 KB,完整開啟的成本卻隨資料等比增長

那個平坦的成本,才是值得圍繞它設計的屬性。一道建置在 GetSheetNames 上的上傳閘,在一份 200 列的檔案與一份 200 MB 的檔案上行為一致,所以一批中最慢的檔案,不再為「一份檔案到底值不值得處理」這個決定定調

一次呼叫涵蓋 .xls、.xlsx 與範本格式

在 XLS 外觀層上,TXLSWorkbook.GetSheetNames 讀的不只是 .xls。它也接受以 zip 為基礎的 .xlsx、.xlsm、.xltx 與 .xltm,只從封存裡抽出 workbook.xml。對真正的 .xls 輸入,它掃描 BoundSheet 記錄,並在全域子串流的第一筆 EOF 記錄停止,所以一份大型二進位檔案仍只花它開頭的幾 KB。XLSX 外觀層帶有一項對長時間執行的服務程式碼來說、比表面看來更重要的保證:TXLSXWorkbook.GetSheetNames 既不重設也不填入活頁簿執行個體,所以一個已經持有開啟文件的執行個體,可以探測其他檔案而不打擾手上的那份。GetODSSheetNames 把同樣的做法套用到 OpenDocument 封包上,而這些呼叫每一個都有串流多載,讓你能檢查一份從不落到磁碟的上傳

var
  Book: TXLSXWorkbook;
  Names: TStringList;
  I: Integer;
begin
  Names := TStringList.Create;
  Book := TXLSXWorkbook.Create;
  try
    if Book.GetSheetNames('upload-7f3a.xlsx', Names) <= 0 then
      raise Exception.Create('unreadable workbook package');
    if Names.IndexOf('Mapping') < 0 then
      raise Exception.Create('required Mapping sheet is missing');
    for I := 0 to Names.Count - 1 do
      Writeln(Format('sheet %d: %s', [I, Names[I]]));
  finally
    Book.Free;
    Names.Free;
  end;
end;

同樣的呼叫造就了一個好的桌面匯入對話框。列出工作表、讓使用者選一個,並且只在選擇做出後才為完整開啟付費。對一份五十個工作表的活頁簿,差別是看得見的:一個立刻出現的挑選器,相對於一個在整份檔案於背後載入時卡住的挑選器

啟用巨集的 .xlsm 檔案與範本格式,列出起來與普通 .xlsx 完全一樣,因為無論封包裡是否搭載了一個 vbaProject.bin,目錄都位於同一個 workbook.xml 裡。因此一條上傳管線可以為了路由而列舉巨集活頁簿的工作表,從不碰觸巨集承載,也從不做任何會執行它的事,並把巨集政策的決定留給實際開啟檔案的那個階段

讀取回傳值而不自欺

回傳慣例在 HotXLS 之間並不一致。有些呼叫在成功時回傳 1,其他的回傳一個計數,所以對列舉函式來說,唯一站得住腳的檢查是把任何零或以下的值當作失敗,並伴隨清空的字串清單。請抗拒把一份空清單讀成「一份沒有工作表的活頁簿」的誘惑。ECMA-376 與 BIFF8 規格都要求一份有效活頁簿至少要有一個工作表,所以零個名稱永遠表示讀取失敗,絕不表示檔案合理地為空

一次失敗的列舉本身就是一個值得保留的訊號。一份無法通過呼叫的 .xlsx 檔案是少數幾種特定東西之一:被截斷、根本不是 OOXML 封包(其他系統誤標的 CSV 匯出在這裡不斷出現),或是一個加密封包。把它們分辨開來是下一項檢查的工作。把被拒檔案的開頭幾個位元組與失敗一起記錄下來,通常能把一條支援討論串變成單一則訊息

在路由之前偵測加密封包

一份加密的 .xlsx 不是 zip。它是一個包裝了 EncryptionInfoEncryptedPackage 串流的 OLE 複合檔案,所以 GetSheetNames 看不進去,並像任何其他無法讀取的檔案一樣回傳失敗。CanReadEncrypted 測試的就是那種容器形狀,這讓上傳能刻意路由一份加密檔案,而不是吞下一個來自某個工作者深處的通用讀取錯誤:

Delphi 進件分流流程:以 HotXLS 的 CanReadEncrypted 與 GetSheetNames 把上傳導向需要密碼、無法讀取或正常
CanReadEncrypted 先跑,因為加密的 OOXML 檔是 OLE 容器,列出呼叫看不進去
type
  TIntakeRoute = (irNormal, irNeedsPassword, irUnreadable);

function ClassifyUpload(const FileName: string; Names: TStrings): TIntakeRoute;
var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    // 加密的 OOXML 是 OLE 容器,不是 zip:先檢查,
    // 因為列舉呼叫無法看進去的內部。
    if Book.CanReadEncrypted(FileName) then
      Exit(irNeedsPassword);
    if SameText(ExtractFileExt(FileName), '.ods') then
    begin
      if Book.GetODSSheetNames(FileName, Names) <= 0 then
        Exit(irUnreadable);
    end
    else if Book.GetSheetNames(FileName, Names) <= 0 then
      Exit(irUnreadable);
    Result := irNormal;
  finally
    Book.Free;
  end;
end;

加密正是 HotXLS 刻意不對稱之處,所以路由必須尊重這一點。舊式的 .xls 加密(RC4、RC4 CryptoAPI、XOR)是可讀的:TXLSWorkbook.Open(FileName, Password) 以一個儲存的密碼解密,而那些檔案可以留在自動化路徑上。加密的 OOXML 封包則走另一個方向。HotXLS 能用 SaveAsEncrypted 寫出一份,卻無法讀回一份。OpenEncrypted 在拿到加密封包時會引發 EXlsxEncryptionNotImplemented,這也是為什麼一個誠實的上傳設計會把加密的 .xlsx 送去給一個有 Excel 的人,而把帶密碼的 .xls 留在程式碼裡

對批次工作而言,這個分類器之所以有其一席之地,是因為它能在任何工作者開始真正處理之前,跑過整個內送目錄,因為每次探測大約只花一次檔案開啟與幾 KB 的讀取。把它前置,改變了營運真正在意的那個失敗模式。與其在凌晨三點某個工作死在 600 個檔案中的第 412 個上,你會得到 412 個檔案排入佇列、5 個在上傳處被拒絕、而且每個都附帶一個原因。同樣的程式庫呼叫,好得多的營運故事

列舉呼叫無法回答的問題

名稱與順序就是你得到的全部。列舉呼叫對可見性什麼也不說,所以隱藏與 very-hidden 的工作表,在清單裡看起來和其他的一樣。它們不回報任何已用範圍尺寸、儲存格計數,也不回報任何文件屬性。docProps/core.xml 部分也很小,但目前沒有僅屬性的探測,所以作者與標題等中繼資料仍要花一次完整 Open。與之共處的乾淨方式,是讓便宜的事實為每個檔案做路由,並把昂貴的事實保留給通過路由的檔案。對於確實進入深度讀取的檔案,一份大型 .xls 的唯讀掃描在 _DisableGraphics := True 之下會明顯快得多,它會略過 OfficeArt 解析。只是絕不要從那個執行個體儲存:它所略過的繪圖圖層已從模型中消失,而儲存會把它從檔案中丟掉

通過分診的檔案通常會進入更深入的分析。活頁簿稽核與轉換工作台涵蓋了在完整開啟獲得正當性之後值得收集的每個工作表計數器,而大型活頁簿效能指南則涵蓋了如何讓那次完整開啟保持快速

HotXLS 是供 Delphi 與 C++Builder 使用的原生 Object Pascal 試算表程式庫;完整的 API 表面(包含此處展示的檢查呼叫)記錄於HotXLS Delphi Component 產品頁