技術記事

HotXLSでメタデータのみと選択的シート読み込みを行う方法

「このワークブックはまだTaxRateという名前を定義しているか?」という問いには、XLSXパッケージのごく小さな部分が答えます。シート名、その隠しまたは完全隠しの状態、そしてすべてのワークブックレベルの定義名は、xl/workbook.xml(ECMA-376 Part 1, §18.2)に住んでおり、たいてい数キロバイトです。セルは別の場所にあり、シートごとのxl/worksheets/sheetN.xmlパートと共有文字列テーブルで、ほぼすべてのバイトはそこにあります。何かを尋ねる前に完全なワークブックを組み立てることを頑なに要求するローダーは、最初の数キロバイトがすでに決着させた問いのために、フォルダ内の全ファイルに対して、そのすべてを展開しパースします

効いてくる分岐は、ワークブックの形を読むことと、その重さを読むことの間にあります。形の後で止まれるローダー、選ばれたシートのサブセットの重さをロードできるローダー、そして後からもう1枚のシートを取ってこられるローダーは、「ワークブックを開く」を全か無かのステップから、呼び出しごとに範囲を決められるものへ変えます。同時に2つの罠も運んできます。1つはインデックスの基数について、もう1つはソースファイルがどれだけの間その場に留まるべきかについてです

メタデータのみのOpenが読むものと、止まる場所

Openの前にTXLSXWorkbook.MetadataOnlyをTrueに設定すると、HotXLSはワークブックパート、スタイル、テーマ、定義名、シート名と可視性の状態、接続、外部リンク、そしてdocProps/core.xmlとdocProps/app.xmlからのドキュメントプロパティをパースします。その後、すべてのワークシートのセルXMLと、シートごとの衛星パート、つまりコメント、図面、テーブルをスキップします。共有文字列テーブルもスキップされますが、これは聞こえ以上に効いてきます。テキストの多いワークブックでは、その1つのパートがワークシートに並ぶサイズになり得るからです

戻ってくるワークブックは、構造的に完成しています。Sheets.Countは正しく、各シートは本物の名前とVisibilityを持ち、DefinedNamesは埋まっており、ExternalLinksはリンク先をすべて列挙し、Title、Author、LastModifiedByはファイルが宣言したものを運びます。ただ、すべてのシートがセルゼロであるだけです

HotXLS MetadataOnly が Delphi の XLSX アーカイブでワークブックプロパティ、シート名、スタイル、リンクを読み、ワークシートのセル XML の前で停止する様子
MetadataOnly はアーカイブのすべての構造パートを読み、ワークシートのセル XML の前で止まります。Delphi のワークブックは、シート名、可視性、定義名、スタイルを持ち、セルゼロで戻ってきます

監査が走るのは、まさにこのデータの上です。どのワークブックがシートを隠しているか? どれがまだTaxRateを定義していて、それは何に向けられているか? どれが昨年引退したファイル共有へリンクしているか? 下のスキャナーは、1つのファイルに対して3つすべてに答えます。2つのインデックスの流儀が並んでいることに注意してください。Sheets[]はXLSエンジンに合わせた1始まりであり、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;

スキャナーがBooleanのVisibleではなくVisibilityを読むのは、意図的なことです。Visibleは両方の隠し状態でFalseであり、監査が最も旗を立てたいのは完全隠しのシートです。Excelの「再表示」ダイアログはそれを列挙せず、VBAかファイルの直接編集だけがそれを戻します

SelectedSheetsで40枚のうち2枚を読み込む

SelectedSheetsは、ワークブックが所有するTStringListです。少なくとも1つの名前を保持していると、Openはその名前のシートについてだけセルXMLをパースし、他のすべてのシートは、正しい名前と可視性、そしてセルなしで作成されます。空のリスト、つまりデフォルトは、全シートをロードします。名前の検索にはリスト自身のIndexOfが使われるため、デフォルト設定では照合は大文字小文字を無視します。これは、1つのワークブック内の2つのシート名が大文字小文字だけで違い得ないという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コード

2つのスイッチは、一方向にのみ相互作用します。MetadataOnly = Trueは選択を上書きして全シートをスキップするため、両方を設定したワークブックは、どこにもセルなしで戻り、選択は、単純に格上だったのに壊れたように見えます。1つのOpenには1つだけ使ってください。発見にはメタデータのみを、狙った作業には選択をです

選択的読み込みは、ParallelParseがオンのとき、並列パースの段階も通ります。選ばれなかったシートはワーカージョブの内側でスキップされるため、スレッドは頼んだシートに時間を使います。ソースがバイナリの.xlsbワークブックのときも、同じ2つのプロパティが適用されますが、スコープ規則が1つあります。VBAプロジェクトを運ぶ.xlsbは、この2つのオプションなしで開かなければなりません。どちらかが設定されていると、Openは選択的読み取りを非対応として報告します

HotXLS SelectedSheets が Delphi で 40 枚のうち 2 つの名前付きシートのセルをパースし、LoadSheet がスキップされたシートを無変更のソースファイルから実体化する様子
SelectedSheets は名前付きのシートだけセル XML をパースし、残りは名前と可視性を保つ空のスタブとして残します。LoadSheet は後でスタブを実体化します。ソースファイルがまだディスク上にあり無変更である場合に限ります

LoadSheet(1)が間違ったシートをロードする理由

LoadSheetが0始まりの位置を受け取るのに対し、Sheets[]は1始まりだからです。LoadSheet(1)はワークブックの2番目のシートを実体化します。しかも、既にセルを持つシートのロードはTrueを返すno-opであるため、混ざったインデックスは、ユーザーがクリックしたシートが空のままなのに、成功に見えます。変換は、自分のコードの中の、正確に1つの場所に置くべきものです

2つ目の罠は、ファイルの寿命です。LoadSheetは、呼び出しの間アーカイブを開いたままにしません。名前でオリジナルファイルを開き直し、ワークブックパート、スタイル、共有文字列をパースし、それから要求されたワークシートをパースします。そのファイルを信じる前に、HotXLSはサイズと最終書き込み時刻を、Openで捕捉した値と比較します。その間に別のプロセスがワークブックへ上書き保存していた場合、LoadSheetは、別バージョンのセルを既に保持しているモデルへ混ぜ込む代わりに、Falseを返します

procedure TReportForm.ShowSheetNo(SheetNo: Integer);  // SheetNoはSheets[]と同じく1始まり
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の呼び出しごとに共有文字列テーブルが再パースされるため、10枚のシートを1枚ずつ実体化すると、そのパートの代金を10回払います。ユーザーがときどきタブをもう1枚開くビューアなら、十分に安い価格です。シートの集合が既に分かっているなら、代わりにSelectedSheetsで開き直します。アーカイブの通過は1回、共有文字列のパースも1回です。コンテンツの違いもあります。LoadSheetが運ぶのはシートのセル、値、数式、フォーマットであり、コメント、図面、テーブルはOpenと一緒に届きます。コメントやテーブルをコードが読むシートは、最初から選択に入れておくべきです

ラウンドトリップが触れてもいないパートを変える理由

すべてを再生成する保存は、変化していないバイトを書き直します。テーマを展開し、オブジェクトモデルを組み立て、シリアライズし、再圧縮すれば、誰もテーマを編集していなくても、結果はオリジナルと違います。VBAプロジェクトとピボットキャッシュでは、うるさいdiff以上のコストが掛かります。VBAプロジェクトは独自の内部ストリームを持つバイナリ複合ファイルであり、ピボットキャッシュは、ピボットテーブル定義が位置でアドレスするフィールドを持つ、ソースデータのスナップショットです。どちらも誰も変更していないときの最も安全な扱いは、そのバイト列に手を付けないことです

デフォルトがTrueのPreserveRawPartsは、SaveAsに、未変更パートの圧縮済みバイト列をソースアーカイブからそのままコピーさせます。これが覆うのは、テーマ、VBAプロジェクト、接続、チャート、埋め込みメディア、プリンター設定、ピボットテーブル、ピボットキャッシュの定義とレコードであり、それぞれ、モデルが未変更だと言っている間だけです。これらのパートは、経路の途中で展開も再圧縮もされません。プロパティをFalseにすると、すべてのパートの完全な再シリアライズが強制されます。出力を、オブジェクトモデルの含むものだけに反映させたいときの手の届くスイッチです

rawコピーの経路は、LoadSheetと同じファイル検査を使います。ソースは、Open時に見たサイズとタイムスタンプのまま存在していなければなりません。そうでないとき、SaveAsはそれらのパートを普通にシリアライズし、ファイルはまだ完全です。常にシリアライズ経路を通るケースが2つあります。ソースがバイナリの.xlsbだったワークブック(読み込み時にXMLへ変換される)と、StrictOOXMLが設定された保存(パートの名前空間が変わる)です

部分的にロードされたワークブックの保存には、決断が要ります。.xlsbソースでは、まだ繰り延べのシートが残っている間、SaveAsは拒否し、Diagnosticsに「Load every deferred binary worksheet before saving」を記録します。.xlsxでは、選択的にロードされたワークブックを読み取りビューとして扱います。スタブシートにはライターが出力すべきセルがないため、スタブごとに先にLoadSheetを呼ぶか、編集と保存の前に選択なしで開き直します

意図的に開き直す発見パイプライン

持ちこたえるパターンは、2つのパスを持ちます。パス1はすべてのファイルをメタデータのみで開き、判断します。パス2は、一致したファイルだけをシート選択付きで開き直し、仕事をします。開き直しは、パス1が何を避けたかを数えるまで無駄に見えます。2回目のOpenだけがセルをパースし、しかもパス1が選んだシートだけをパースするのです

Delphi における 2 パスの HotXLS 発見パイプライン。メタデータのみのパスがどのワークブックとシートが重要かを判断し、狙いを絞ったパスが選択されたシートだけをパースする
パス1はディレクトリをメタデータのみでスキャンし、どのファイルとシートが重要かを判断します。パス2は一致したものをシート選択付きで開き直します。セル XML をパースするのは、2回目のオープンだけです
procedure ProcessInvoiceFile(const FileName: WideString);
var
  Probe, Workbook: TXLSXWorkbook;
  Wanted: TStringList;
  I: Integer;
begin
  Wanted := TStringList.Create;
  try
    // パス1:発見。セルなし
    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;                               // このファイルにパースする価値のあるものはない

    // パス2:選ばれたシートのセルだけ
    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を通じて1枚ずつ育てられます。1枚か2枚ならそれも合理的ですが、パイプラインにはきれいな開き直しのほうが合います。各シートのコメントとテーブルも一緒に運んでくれます

ほかの大ファイル向けレバーとの関わり

選択的読み込みが減らすのは、パースするものです。ロード後のシートのコストには何もしませんし、仕事が全シートの全セルを要するときには助けになりません。そのケースはストレージの話であり、行ブロックセルストレージと範囲ベースのスタイルの記事で、そしてパースのスループットの話であり、並列XLSXパースとメモリアロケータで扱います。仕事がまったく編集のない純粋な読み取りなら、ストリーミングダイレクトリーダーがワークブックのオブジェクトモデルを丸ごとスキップし、それを組み立てるどんなロード戦略よりも良い道具です

これらすべては、スキャンを行うサーバーにExcelのインストールなしで、自分自身のDelphiや C++Builder の実行ファイルの中で走ります。対応フォーマット、エディション、ライセンスは、HotXLS Delphiスプレッドシートコンポーネントページに一覧されています