HotXLS 通过单个拉取式行游标 TXLSRowCursor 读取 .xls、.xlsx、.xlsm、.ods、CSV 与 TSV 源,其 FindFirst 与 FindNext 每次推进一个逻辑行,且只有该行驻留内存。一个六值状态机把首行之前与 EOF、已取消、已故障区分开,旧的回调读取器如今只是同一游标之上的适配器
做过导入功能的人都熟悉这个场景。一个 200 MB 的 .xlsx 到了,你接上 OnCell 处理器,而“读出来”之后的第一个需求是“读到第一百条冲正过账就停”。此刻代码的形态开始跟你作对:循环住在库里,你的处理器得立个标志,之后的每次回调照样触发直到解析器注意到,而累积状态——目前命中几条、哪一列匹配、下一步做什么——只能放在一个类的字段里,这个类存在的唯一理由是给回调一个坐的地方。这里面没有一件事是解析问题。这是控制流问题,而拉取式游标正是把它拿掉的东西
200 MB 时推送回调的真实代价
推送反转控制,而过滤或联接型调用方恰恰承受不起这种反转。回调 API 下库拥有循环,于是调用方用不了 Break,无法交错两个源,无法把读取器交给一个等待被驱动的例程,不缓冲就表达不了“先偷看下一行再决定”。代价不在吞吐——写得好的 SAX 回调路径流得很顺——在于每个不平凡的消费者都自己长出一台小状态机,来模拟那台它不被允许写的循环。再乘以四种文件格式、每种历史上各有扫描入口,过滤、公式与错误语义就开始在它们之间漂移,而这正是 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); // 只解码这两列
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 之后重新可写,于是一次扫描永远不可能混用两套解码契约。如果你只要清单而不要行,仅元数据与选择性工作表加载是更便宜的入口
每种格式一个后端,各自一条扫描循环
每种格式在 HotXLS 内恰好有一台前向扫描器,拉取式游标与回调读取器驱动的是同一台。TXLSXForwardRowBackend 是 ECMA-376 Part 1 §18.3 工作表部件唯一的工作表 SAX 状态机,持有 XML 读取器、共享公式表与富文本解析器,每次调用恰好推进到一个物理 <row> 边界。TXLSBiffForwardParser 掌管 [MS-XLS] 记录流的全局区、工作表选择与行推进;让它可暂停产生了整个设计中最尖锐的约束,因为一个缓存字符串公式是一条 Formula 记录紧跟一条 String 记录,按行挂起的挂点绝不能落在两条记录之间。TXLSForwardTextBackend 持有一个 BOM 感知的读取器、当前分隔符与一条逻辑记录——CSV 从首条记录嗅探逗号、分号、制表符或竖线且忽略引号内字符,多行带引号字段以 #10 拼接,于是行号跟踪逻辑记录而非物理换行。TXLSForwardOdsBackend 为 OpenDocument §9 表格持有单个物理行模板,把 table:number-rows-repeated 当作剩余计数而非展开,并越过被覆盖单元格而不产生值。流式直接读取器共享同一共享字符串与日期样式加载器
为什么是六个状态而不是一个 Eof 标志?
因为单个布尔让四种不同情形不可区分,而调用方对它们全都猜错。TXLSRowCursorState 把它们显式命名
xrcsClosed——没有打开的源xrcsBeforeFirst——已打开或已重定向,尚未读到任何行xrcsActive——正站在一个有效行上xrcsEof——工作表被消费到末尾xrcsCancelled——调用方有意停止了本次扫描xrcsFaulted——扫描失败,原始异常已被抛出
最后这个区分才是生产中要紧的。工作表部件缺失或扫描启动失败会保留其 EReadError 并把游标移入 xrcsFaulted;它绝不会被降级成调用方会读作“这张表是空的”的普通 False。Cancel 刻意比 Close 窄:它关闭当前工作表后端及其 inflate 子流并使当前行失效,但不释放 ZIP 包或源流,调用第二次是空操作。取消后想恢复须显式调用 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; // 只关闭工作表后端及其
Break; // inflate 子流;幂等
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。这份契约要说实话——它是生命周期快速失败,不是线程安全保证,它不许可你在第一个线程推进游标时从第二个线程读行
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 一起读,后者统计实际开始了多少次工作表扫描。两条警告让这个数字保持诚实:它是估算而非精确堆核算,且自最近一次 Open 起单调不减,所以它是调试与回归仪表而非实时仪表。大簿上时间与字节去向的全景见Delphi 中的大工作簿性能
推送读取器变成了适配器,以及游标不做什么
TXLSForwardReader 不再携带独立的 XLSX、BIFF 与文本扫描入口。它配置一个游标、走它、把当前行翻译成 OnSheet 与 OnCell 事件,所以两个门面在过滤、公式状态或错误处理上再也不可能漂移。升级前有两条后果值得知道:回调 SheetIndex 现在在 TXLSForwardReader 上统一为 1 起(TXLSDirectReader 保留其既有的 0 起事件契约),且 OnSheet 先于 SelectSheet 触发,于是设置 SkipSheet 意味着该工作表部件根本不会被打开或解压。边界同样显式:扫描进行期间工作簿不可修改,取消后须显式重启,BIFF 前向路径绝不反编译公式记号,所以经典公式单元格报告 HasFormula 为 true 而 FormulaTextAvailable 为 false,递给你缓存结果而不是编造一个空公式字符串。该游标及其适配器通过了 Delphi Win32 与 Win64 外加 C++Builder 37.0 Win64 静态包上的 1,298 项检查
如果你在掂量拉取式游标与手头加载器的取舍,该问的不是谁解析得更快,而是谁让你写出你真正需要的退出条件。组件全部细节、支持的 IDE 版本与许可见 HotXLS Delphi 电子表格组件页