HotXLSは、.xls、.xlsx、.xlsm、.ods、CSV、TSVの各ソースを単一のプル行カーソルTXLSRowCursorを通じて読みます。そのFindFirstとFindNextは1論理行ずつ前進し、その行だけがメモリに滞在します。6値の状態マシンが先頭前をEOF、キャンセル、故障から分離し、旧来のコールバックリーダは今や同じカーソル上のアダプタです
シナリオはインポート機能を出荷したことがある人には馴染みのものです。200 MBの.xlsxが届き、OnCellハンドラを配線し、「読め」の次の要件が「逆転仕分けの最初の100件で止まれ」になります。ここでコードの形が敵に回ります。ループはライブラリの中に住み、ハンドラはフラグを立てなければならず、パーサが気づくまでその後のコールバックは全部発火し続け、蓄積された状態——これまでのヒット数、どの列が一致したか、次に何をするか——は、コールバックの居場所を提供するためだけに存在するクラスのフィールドに住まなければなりません。そこにはパースの問題は何もありません。制御フローの問題であり、プルカーソルが取り除くのはまさにそれです
プッシュコールバックが200 MBで実際に払う代償
プッシュは制御を反転させ、反転こそフィルタや結合を行う呼び出し側が耐えられないものです。コールバックAPIではライブラリがループを所有するので、呼び出し側はBreakを使えず、2つのソースを交互に処理できず、駆動されることを期待するルーチンへリーダを渡せず、バッファリングなしに「決める前に次の行を覗く」を表現できません。代償はスループットではありません。よく書かれたSAXコールバックパスは十分にストリームします。問題は、自前のループを書くことを許されなかったループを模倣する小さな状態マシンを、自明でない消費者すべてが育ててしまうことです。それを4つのファイル形式——それぞれ歴史的に独自の走査エントリポイントを持つ——に掛け合わせると、フィルタ、数式、エラーの意味論が形式間で乖離し始めます。HotXLSが閉じようとしたのはまさにその乖離です
プルカーソルは呼び出しコードをどう変えるか
ループをあなたに返し、それとともに普通のPascal制御フローを返します。TXLSRowCursor.Openはファイル名かTStreamを受け取り、形式を検出し、共有文字列と日付スタイルのメタデータを一度にロードし、シート1を選びます。SelectSheet(1始まり)やSelectSheetByNameは別のワークシートへ再ターゲットし、カーソルを先頭前へリセットします。FindFirstとFindNextは次の値が入った行に位置します。デコード可能なセルを持たない行は飛ばされるので、RowIndexは跳び得ます。現在行はCellCount、Cells[]、ValueByCol[]として露出し、列軸ではすべて1始まりです。ループを抜けるのはBreakです
var
Cursor: TXLSRowCursor;
Hits: Integer;
begin
Cursor := TXLSRowCursor.Create;
try
Cursor.FirstRow := 2; // ヘッダ帯を飛ばす
Cursor.IncludeColumn(1); // この2列だけデコードする
Cursor.IncludeColumn(7);
if not Cursor.Open('postings-200mb.xlsx') then
Exit;
if not Cursor.SelectSheetByName('Ledger') then
Exit;
Hits := 0;
if Cursor.FindFirst then
repeat
if VarToStr(Cursor.ValueByCol[7]) = 'REVERSED' then
begin
Inc(Hits);
if Hits = 100 then
Break; // 普通の Break。中断フラグも番兵も不要
end;
until not Cursor.FindNext;
finally
Cursor.Free; // デストラクタがパスを終了させる
end;
end;
射影と範囲はパスの前に設定し、後でフィルタするのではありません。FirstRow、LastRow、IncludeColumn、ClearColumnProjection、IncludeFormulaText、DetectDates、DetectTextTypesはすべてバックエンド内部で尊重され、選択されていない列はそもそも値、数式文字列、リッチテキストペイロードを割り当てません。回帰スイートは、16 KiBの数式とキャッシュ文字列が、列が射影されていなければ決して実体化しないことでこれを証明します。これらのオプションはパスがアクティブな間は意図的に凍結され、EOF時、SelectSheet時、Close後に再び書き込み可能になります。1つの走査が2つのデコード契約を混ぜることは決してありません。行ではなくシート一覧だけが必要なら、メタデータのみと選択的シートロードがより安い入口です
形式ごとに1つのバックエンド、それぞれ1つの走査ループ
HotXLS内部ではすべての形式がちょうど1つの前方スキャナを持ち、プルカーソルもコールバックリーダもその同じスキャナを駆動します。TXLSXForwardRowBackendはECMA-376 Part 1 §18.3のシートパートに対する唯一のワークシートSAX状態マシンで、XMLリーダ、共有数式テーブル、リッチテキストパーサを保持し、呼び出しごとにちょうど1つの物理<row>境界へ前進します。TXLSBiffForwardParserは[MS-XLS]レコードストリームのグローバル、シート選択、行前進を所有します。これを一時停止可能にしたことが設計全体で最も鋭い制約を生みました。キャッシュされた文字列数式はFormulaレコードの直後にStringレコードが続く形なので、行ごとの停止点は決して2つの間に着地してはならないのです。TXLSForwardTextBackendはBOM対応リーダ、アクティブな区切り文字、1つの論理レコードを保持します。CSVは引用文字を無視しながら最初のレコードからカンマ、セミコロン、タブ、パイプを判別し、複数行の引用フィールドは#10で結合されるので、行番号は物理改行ではなく論理レコードを追跡します。TXLSForwardOdsBackendはOpenDocument §9テーブル用の単一の物理行テンプレートを保持し、table:number-rows-repeatedを展開ではなく残りカウントとして扱い、値を出力せずにカバー済みセルを通過します。ストリーミング直接リーダは同じ共有文字列と日付スタイルのローダを共有します
1つのEofフラグではなく6つの状態なのはなぜか
単一のbooleanは4つの異なる状況を区別できなくなり、呼び出し側はそのすべてについて間違って推測するからです。TXLSRowCursorStateはそれらを明示的に名指します
xrcsClosed— 開かれたソースがないxrcsBeforeFirst— 開いたか再ターゲットした直後で、まだ行を読んでいないxrcsActive— 有効な行の上に立っているxrcsEof— シートを終端まで消費したxrcsCancelled— 呼び出し側がパスを意図的に止めたxrcsFaulted— パスが失敗し、元の例外が投げられた
本番で効いてくるのは最後の区別です。欠落したワークシートパートや失敗したパス開始は自分のEReadErrorを保持し、カーソルをxrcsFaultedへ動かします。「このシートは空だった」と読める素のFalseへ格下げされることは決してありません。Cancelは意図的にCloseより狭いです。現在のワークシートバックエンドとそのinflateサブストリームを閉じて現在行を無効化しますが、ZIPアーカイブやソースストリームは解放せず、2回呼んでもノーオペレーションです。キャンセル後はSelectSheetを明示的に呼んで再開します。カーソルがあなたの代わりに静かにパスを再開することはありません。ストリーム所有権も同じ防御規則に従います。xsoBorrowedがデフォルトで、クローズ時にストリーム位置を復元し、xsoOwnedはOpenがすでに成功した後にのみ所有権を移します。失敗したオープンが呼び出し側の保持するストリームを解放することは決してありません
var
Cursor: TXLSRowCursor;
Src: TFileStream;
begin
Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
try
Cursor := TXLSRowCursor.Create;
try
// xsoBorrowed: カーソルは Src を決して解放せず、Close は
// Open 時点のストリーム位置を復元する
if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
Exit;
if Cursor.FindFirst then
repeat
if UserPressedStop then
begin
Cursor.Cancel; // ワークシートバックエンドとその inflate
Break; // サブストリームだけを閉じる。冪等
end;
until not Cursor.FindNext;
case Cursor.State of
xrcsEof: Log('sheet consumed to the end');
xrcsCancelled: Log('stopped by the operator');
xrcsFaulted: Log('pass failed; the EReadError was already raised');
end;
finally
Cursor.Free;
end;
finally
Src.Free; // 依然こちらのもの、依然有効、位置も復元済み
end;
end;
現在行をコピーせずに借りる
IXLSRowCursorViewはセル配列を複製せずに行を別のルーチンへ渡します。ビューはカーソルポインタとUInt64世代カウンタを保持する共有ガードを持ちます。前進、シート選択、キャンセル、クローズ、カーソルの破棄はすべてその世代をインクリメントし、破棄はさらにガードのオーナーをクリアします。したがって古くなったビューは解放済みメモリを読めません。Validはいつでも呼べる例外なしのプローブであり、他のすべてのメンバーは先に検証してEXLSRowCursorViewInvalidatedを投げます。この契約が何であるかは正直に言ってください。ライフタイムのフェイルファストであって、スレッド安全性の保証ではありません。1番目のスレッドがカーソルを前進させている間に2番目のスレッドから行を読む許可にもなりません
var
View: IXLSRowCursorView;
Cell: TXLSRowCursorCell;
I: Integer;
begin
if Cursor.FindFirst then
repeat
View := Cursor.CurrentRowView; // 借りる。セル配列はコピーされない
for I := 0 to View.CellCount - 1 do
begin
Cell := View.Cells[I];
if Cell.HasFormula and not Cell.FormulaTextAvailable then
UseCachedResult(Cell.Value) // BIFF フォワード読み取りはトークンではなく
else if Cell.Kind = xdkEmpty then // キャッシュ結果を保持する
UseStyleOnly(Cell.StyleIndex) // Blank / MulBlank も実セル
else
UseValue(Cell.Col, Cell.Value);
end;
until not Cursor.FindNext;
// インターフェースはループより長生きするが、裏の行はしない
if not View.Valid then // Valid は決して投げない。今 Cells[] を呼べば
View := nil; // EXLSRowCursorViewInvalidated が投げられる
end;
PeakRowBufferedBytesと、それが証明することを許されたもの
PeakRowBufferedBytesは、メモリが行数ではなく行幅を追跡することを示すために存在します。現在の出力行のセルレコード、Variant、数式文字列、リッチテキストペイロードを累積し、形式固有のワーキングセット——CSVの論理レコード、ODSの物理行テンプレート、BIFFのレコードピーク、デコード中のXLSX生セル——を折り込みます。実際に開始されたワークシートパスの数を数えるSheetPassesStartedとともに読んでください。これを正直に保つ2つの留保があります。数値は正確なヒープ会計ではなく推定であること、そして最新のOpen以降で単調であること。したがってライブゲージではなくデバッグと回帰の計器です。非常に大きなブックで時間とバイトがどこへ向かうかのより広い展望は、Delphiでの巨大ブックのパフォーマンスを参照してください
プッシュリーダはアダプタになった。そしてカーソルがしないこと
TXLSForwardReaderはもはやXLSX、BIFF、テキストの個別の走査エントリポイントを持ちません。カーソルを構成し、それを歩き、現在行をOnSheetとOnCellイベントへ翻訳します。だから2つのファサードがフィルタ、数式状態、エラー処理で乖離することはもうありません。アップグレード前に知っておくべき帰結が2つあります。コールバックのSheetIndexはTXLSForwardReader上で均一に1始まりになりました(TXLSDirectReaderは既存の0始まりイベント契約を保ちます)。そしてOnSheetはSelectSheetの前に発火するので、SkipSheetを設定するとワークシートパートはそもそも開かれず展開もされません。境界も同じく明示です。パスがアクティブな間にブックを変更してはならず、キャンセルからの再開は明示的な再起動を要求し、BIFFフォワードパスは数式トークンを決して逆コンパイルしないので、クラシックな数式セルはHasFormulaが真でFormulaTextAvailableが偽と報告し、空の数式文字列をでっち上げる代わりにキャッシュ結果を渡します。行カーソルとそのアダプタは、Delphi Win32とWin64に加えてC++Builder 37.0 Win64静的パッケージ上で1,298検査に通りました
今持っているローダとプルカーソルを比較しているなら、問うべき問いはどちらが速くパースするかではなく、どちらが実際に必要な終了条件を書かせるかです。コンポーネントの詳細、対応IDEバージョン、ライセンスはHotXLS Delphi spreadsheet component pageにあります