技術記事

Delphiから大規模なXLSX内の1つのワークシートだけをパッチする

HotXLSは、ファイルの残りの部分を解析したり再圧縮したりすることなく、既存のXLSXパッケージ内の1つのワークシートだけを書き換えられます。TXLSDirectWriter.BeginPatchはソースパッケージを開き、対象のシートを除くすべてのエントリを、その圧縮バイト列のまま引き継いでコピーします。そして、通常のAddSheetAddRowWrite*の呼び出しを通じて、その1枚のシートだけを書き直せるようにします。チャート、ピボットキャッシュ、テーマ、スタイル、共有文字列は一切解凍されません

この機能が解決するワークフローは、レポート作成やデータ更新の場面で登場します。ピボットテーブル、スライサー、条件付き書式、そして10年分積み重なった書式設定を抱えたワークブックが、事業部門から届きます。毎晩、1枚のデータシートを最新の数値に差し替えなければなりません。ワークブック全体を読み込んで再保存すると、1ファイルあたり数分かかるだけでなく、より重要な点として、読み込みエンジンが再構築しなければならない機能の忠実度を損なうリスクがあります。パッチ処理は、触れる必要のないものには触れないことで、この両方の問題を回避します

なぜ圧縮バイト列をコピーすることが興味深いのか?

圧縮レベルでコピーされるzipエントリのコストは、ストリームのコピーだけです。同じエントリを通常の書き込みパスに通すと、入力時にinflate、出力時にdeflateのコストがかかり、deflateの方が高くつきます。大きなピボットキャッシュと数十枚の埋め込み画像を持つワークブックでは、この違いが、新しいシートを書き込む時間だけで終わるパッチと、一度も中身を見ないバイト列の再圧縮にほとんどの時間を費やすパッチとの違いになります

HotXLSはこのためにCopyCompressedFromを使用し、ソースエントリの圧縮バイト列をそのままターゲットのアーカイブに書き込みます。異なる圧縮方式や弱い暗号化を使用しているなどの理由でその方法でコピーできないエントリがある場合、ライターは失敗する代わりに、解凍済みのストリームコピーにフォールバックします。ディレクトリマーカーエントリはスキップされます。ライター自身が独自のものを生成するからです

その場で置き換える、あるいは新しいファイルへ書き出す

2つのオーバーロードが、このタスクが取り得る2つの形を扱います。インプレース形式は、結果を元のファイルの隣にある一時ファイルにステージングし、ソースのハンドルを閉じてから削除とリネームを行います。そのため、書き込みの途中でクラッシュしても元のファイルは無傷のまま残ります。明示的なターゲットを指定する形式は、ソースを一切変更せず、シートを置き換えることも新しいシートを追加することもできます:

var
  W: TXLSDirectWriter;
begin
  W := TXLSDirectWriter.Create;
  try
    W.BeginPatch('monthly-dashboard.xlsx', 'Data');   // その場で置き換える
    W.AddSheet('Data');
    W.AddRow(1);
    W.WriteString(1, 'Region');
    W.WriteString(2, 'Revenue');
    W.AddRow(2);
    W.WriteString(1, 'North');
    W.WriteNumber(2, 184320.55);
    W.AddRow(3);
    W.WriteFormula(1, '=SUM(B2:B2)');
    W.Close;
  finally
    W.Free;
  end;
end;

挿入バリアントは、ソースとターゲットのパスに加えてInsertSheetを受け取ります:

  // ソースは変更されない。ターゲットにはExtraという名前のワークシートが追加される
  W.BeginPatch('template.xlsx', 'output.xlsx', 'Extra', True);
  W.AddSheet('Extra');
  W.AddRow(1);
  W.WriteString(1, 'appended by the nightly job');
  W.Close;

挿入は、実質的な帳簿操作の外科手術が必要になる部分です。ライターはxl/workbook.xml内のシートレジストリと、各シートをそのパーツに結び付けるリレーションシップマップを解析し、次に空いているパーツ番号、シート識別子、リレーションシップ識別子を選びます。リレーションシップの種類はソースパッケージの慣習に従うため、strictなISO 29500ワークブックをパッチするとstrictなリレーションシップの種類が出力され、移行用のワークブックをパッチすると移行用の種類が出力されます

パッチが意図的に取り除き、制約するもの

計算チェーンは、どちらのモードでも破棄されます。置き換えモードでは、そのエントリはもはやその形では存在しないシート内のセルを記述しており、挿入モードではシートインデックスのずれによってそもそも無効になります。Excelは次の再計算時にチェーンを再構築するため、これを破棄することは損失ではなく正しい処理です。このパートはコピーの対象から除外され、そのリレーションシップエントリとコンテンツタイプのオーバーライドは外科的に取り除かれます

パッチの内部では、2つの記述上の意味論が変わります。どちらも、パッチは書き直していないパーツを乱してはならないという同じ原則から生じています。文字列は共有文字列テーブルに追加されるのではなく、シートにインライン形式で書き込まれます。ソースのテーブルは手を加えられずに引き継がれるからです。また、StyleIndexは、ライターが構築するスタイルテーブルではなく、ソースパッケージのcellXfs内のエントリを指します。これはつまり、元のワークブックがすでに定義している書式を参照できるということであり、これはたいていデータ更新が求めているまさにその挙動です。しかし同時に、どのインデックスがどの書式に対応するかを知っておく必要があるということでもあります

// パッチ内では、StyleIndexはソースパッケージのcellXfsを指す
// 日付には、そこで日付書式にマップされる明示的なインデックスが必要:
W.WriteDateTime(3, EncodeDate(2026, 8, 22), DateStyleIndexFromTemplate);

// スタイルを指定しないWriteDateTimeオーバーロードはパッチモードでは拒否される。
// ライター自身のスタイルテーブルを前提としているためであり、
// パッチはそのテーブルを一切作成しない

6つの記述用エントリポイントにはゲートが設けられています。テーブル、チャート、画像、コメント、名前付き範囲、セルスタイルの追加は、いずれもパッチモードでは例外を発生させ、さらにクローズ時には、これらのカウンターのいずれかがゼロでない場合に失敗する第二の安全策も備えています。これらの機能はいずれも、パッチが逐語的にコピーするパーツの編集を必要とするものであり、中途半端に編集されたパッケージは、操作を拒否するよりも悪い結果を招きます。1回の操作でパッチできるシートはちょうど1枚だけです

パッチすべき時と読み込むべき時

パッチ処理が適切な手段となるのは、ワークブックが大きく、変更が1枚のシートに限定されており、ファイルの残りの部分がビット単位で無傷のまま残らなければならない場合です。逆に不適切な手段となるのは、変更が複数のシートにまたがる場合、新しい書式や新しいオブジェクトが必要な場合、あるいは通常の読み込みと保存にコストがほとんどかからないほどファイルが小さい場合です。ゼロからの一括生成には、ストリーミング直接ライターで説明したストリーミング方式の方が適しています。同じAddRowWrite*のAPIを共有しているため、両者の間を移動するのは機械的な作業です

完全なオブジェクトモデルが本当に必要な場合の、読み込み済みワークブック内でのシートレベルの操作については、XLSXパッケージ内でのワークシートの複製で扱っています。また、パッチを検討している理由がワークブック全体の処理が遅くなってきたことであるなら、手法を選ぶ前に大規模ワークブックのパフォーマンスにある測定結果とメモリの挙動を読む価値があります

パッチが本当に想定通りに動いたかを検証する

3つのチェックで、ほとんどすべての誤りを捕捉できます。生き残っているはずのパーツがアーカイブ内に実際に存在すること、xl/calcChain.xmlが消えていること、そしてTXLSXWorkbookでファイルを再度開いたときに想定通りのシート数が報告されること——置き換えなら変化なし、挿入なら1つ増加——を確認してください。パッチ後のシートを読み戻して、いくつかの値と数式を比較すれば、検証の輪が閉じます

この機能の開発過程で得られた1つの実装上の詳細は、繰り返し述べる価値があります。同様のzipレベルのコードを書く人なら誰でもはまり得るものだからです。ワークシートのパーツ名はプレフィックスで照合されており、プレフィックス長のオフバイワンエラーがあると述語が決してマッチしなくなり、新しく書き込まれたパーツが既存の名前と衝突してしまいます。そして、同じ名前を持つ最後のエントリを採用するリーダーは、黙って間違ったシートを選んでしまいます。パッチが2枚のシートの内容を入れ替えてしまったように見える場合は、XMLを見る前に名前の照合ロジックを確認してください

インプレースパッチ、ストリーミング書き込み、そして完全なワークブックオブジェクトモデルは、Delphiおよび C++Builder向けの同じライブラリに同梱されています。機能一覧はHotXLS Delphiスプレッドシートコンポーネントページに掲載されています