技術記事

HotXLS Delphi Component: Delphi での sheet listing and lightweight workbook inspection

取り込みルーチンが必要とする問いが、ときには構造だけであることがあります。このワークブックには "Mapping" という名前のシートがあるか、あるいはいくつのタブを持っているか、といったものです。それを Open を呼び出して確認するのは高くつくやり方です。フルオープンは共有文字列テーブルを膨張させ、すべてのスタイルレコードをデコードし、各ワークシートのセルを歩きます。目次だけが欲しいのだと知る方法がないからです。大きなファイルでは、それは数百メガバイトのアロケーションと数秒の CPU 時間を、わずか数キロバイトしか占めないリストを読むために費やすことを意味します。losLab のネイティブ Delphi スプレッドシートライブラリである HotXLS は、そのリストを単独で提供してくれます。GetSheetNames は、ワークブックの順序どおりにワークシート名を返し、セルを 1 つも実体化しません

カタログの読み取りが安価な理由

どちらのスプレッドシートフォーマットも、目次をファイルの先頭近くに置いており、それが listing 呼び出しを賢さではなく速さで済ませてくれる理由です。OOXML パッケージはシートカタログを xl/workbook.xml に保持しており、このパートはワークブックが 10 行を持っていようと 1000万行を持っていようと小さいままです。BIFF8 の .xls は、その BoundSheet レコードを、あらゆるセルデータより前、ワークブックグローバルストリームの先頭に保存します。したがって、listing 呼び出しが避ける作業は、フルオープンに対する丸め誤差程度のものではありません。ファイルのほとんどです。カタログの読み取りは行数にかかわらず同じわずかなキロバイト数のコストで済みますが、フルオープンはデータとともにスケールします。数メガバイトのワークブックでは、その差は触れるバイト数でも確保されるメモリでも、何桁にも及びます

Delphi における HotXLS GetSheetNames が XLSX または XLS ファイルのシートカタログのみを読む様子。完全なオープンは全セルをたどる
カタログは workbook.xml か BoundSheet レコードの中にあり、一覧取得は数キロバイトで済む一方、完全なオープンはデータに比例します

その平坦なコストこそが、それを前提に設計する価値のある性質です。GetSheetNames の上に構築された取り込みゲートは、200 行のファイルでも 200 MB のファイルでも同じように振る舞うため、バッチの中で最も遅いファイルが、そもそもそのファイルを処理する価値があるかどうかを判断するペースを左右することはもうありません

.xls、.xlsx、そしてテンプレートフォーマットを横断する 1 つの呼び出し

XLS ファサードでは、TXLSWorkbook.GetSheetNames は .xls 以上のものを読み取ります。zip ベースの .xlsx、.xlsm、.xltx、.xltm も受け付け、アーカイブから workbook.xml だけを引き出します。本物の .xls 入力に対しては BoundSheet レコードを走査し、グローバルサブストリームの最初の EOF レコードで止まるため、大きなバイナリファイルであっても、そのコストは先頭の数キロバイトだけで済みます。XLSX ファサードは、見た目以上に長時間稼働するサービスコードにとって重要な保証を伴います。TXLSXWorkbook.GetSheetNames はワークブックインスタンスをリセットも populate もしないため、すでに開いた文書を保持しているインスタンスは、手元のものを乱すことなく他のファイルを探れます。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;

同じ呼び出しは優れたデスクトップのインポートダイアログにもなります。シートを一覧表示し、ユーザーに 1 つ選ばせ、その選択が済んだ後にだけフルオープンのコストを支払います。50 シートのワークブックでは、その違いは目に見えます。即座に現れるピッカーと、その裏でファイル全体が読み込まれる間止まってしまうピッカーとの違いです

マクロ有効の .xlsm ファイルとテンプレートフォーマットは、パッケージに vbaProject.bin が同梱されているかどうかにかかわらず、カタログが同じ workbook.xml にあるため、通常の .xlsx とまったく同じように一覧表示されます。したがって、取り込みパイプラインはルーティングのためにマクロワークブックのシートを列挙でき、マクロのペイロードには一切触れず、それを実行してしまうようなことも一切せず、マクロポリシーの判断は実際にファイルを開くステージに委ねることができます

自分自身を欺かずに戻り値を読む

HotXLS 全体で戻り値の慣習は統一されていません。成功時に 1 を返す呼び出しもあれば、件数を返す呼び出しもあるため、listing 系の関数について成り立つチェックは、0 以下のあらゆる値を失敗として扱い、文字列リストをクリアする、というものだけです。空のリストを「シートを持たないワークブック」と読み取りたくなる誘惑には抵抗してください。ECMA-376 と BIFF8 仕様のどちらも、有効なワークブックには少なくとも 1 つのシートを要求しているため、0 個の名前は常に読み取りが失敗したことを意味し、ファイルが正当に空であることを意味することは決してありません

失敗した listing それ自体が、保持しておく価値のあるシグナルです。呼び出しに失敗する .xlsx ファイルは、いくつかの特定のケースのどれかです。切り詰められている、そもそも本物の OOXML パッケージではない(他システムからの誤ラベル付き CSV エクスポートがここで頻繁に現れます)、あるいは暗号化されたコンテナである、といったものです。それらを見分けるのは次のチェックの仕事です。拒否されたファイルの先頭バイトを失敗と一緒にログへ記録しておくと、たいていサポートスレッドは 1 件のメッセージで済みます

ルーティングの前に暗号化コンテナを検出する

暗号化された .xlsx は zip ではありません。EncryptionInfoEncryptedPackage のストリームをラップした OLE 複合ファイルであるため、GetSheetNames はその中を見ることができず、他の読み取り不能ファイルと同様に失敗を返します。CanReadEncrypted はそのコンテナ形状を検査するため、取り込みは、ワーカーの奥深くで発生した汎用の読み取りエラーを飲み込むのではなく、意図的に暗号化されたファイルをルーティングできます

HotXLS CanReadEncrypted と GetSheetNames でアップロードを要パスワード、読取不能、通常へ振り分ける Delphi 受け入れトリアージフロー
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 は zip ではなく OLE コンテナである。先にチェックする。
    // listing 系の呼び出しはその中を見られないため。
    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 はコードの側にとどめておくのです

バッチ処理では、この分類器は、各プローブがおよそ 1 回のファイルオープンと数キロバイトの読み取りしかコストしないため、実際の処理をワーカーが始める前に受信ディレクトリ全体を走査することで、その存在価値を発揮します。これを前段に持ってくることで、運用担当者が実際に気にする失敗のモードが変わります。600 件中 412 件目のファイルで午前 3 時のジョブが落ちる代わりに、412 件がキューに入り、5 件が取り込み時点で理由付きで却下される、という結果になります。同じライブラリ呼び出しでありながら、運用上の物語ははるかに良くなります

listing 呼び出しが答えられない問い

名前と順序が、得られるものの全部です。listing 系の呼び出しは可視性について何も語らないため、非表示のシートや完全に隠されたシートも、他のものと同じ姿でリストに現れます。使用範囲の寸法も、セル数も、文書のプロパティも報告しません。docProps/core.xml パートも小さいものですが、今日時点ではプロパティだけを取り出すプローブは存在しないため、作成者やタイトルのメタデータには依然としてフルの Open のコストがかかります。それとうまく付き合う清潔な方法は、安価な事実にすべてのファイルをルーティングさせ、高価なものはルーティングを生き延びたファイルのために取っておくことです。深い読み取りに進むファイルについては、_DisableGraphics := True を使うと大きな .xls の読み取り専用スキャンが目に見えて速くなります。これは OfficeArt のパースをスキップするためです。ただし、そのインスタンスから保存することは決してしないでください。スキップされた描画レイヤーはモデルから失われており、保存すればそれをファイルから落としてしまいます

トリアージを通過したファイルは、たいてい、より深い分析へと進みます。ワークブック監査と変換ワークベンチは、フルオープンが正当化された時点で収集する価値のあるシート単位のカウンターを扱っており、大規模ワークブックの性能ガイドは、そのフルオープンを高速に保つ方法を扱っています

HotXLS は Delphi と C++Builder 向けのネイティブ Object Pascal スプレッドシートライブラリです。ここで示した検査呼び出しを含む完全な API サーフェスは HotXLS Delphi Component の製品ページで文書化されています