技术文章

Delphi 中读取 Excel 公式缓存值而不重算

HotXLS 是原生 Delphi 和 C++Builder Excel 库,通过 TryGetCachedFormulaValueIXLSFormulaCacheReader 读取 Excel 已存在公式旁边的值。两个入口点都不调计算器、不反编译公式记号、不动脏标志、不把任何东西写回模型,于是一个你只读的工作簿保持你打开它时的样子

驱动这件事的场景平淡且极常见。一个夜间作业打开几百个别人产出的工作簿,从每个里抽一列合计,把数字推进数仓。合计本就坐在文件里,Excel 算好并存了。然而作业向公式单元格要值的那一刻,对这个问题只有一个答案的库就会建依赖图、评估整张表,一个本该 I/O 受限的作业变成了计算基准测试

为什么读一个公式单元格要花一次全量重算?

因为公式单元格上的取值器是在请求产出一个值,而产出值唯一普遍正确的方式是评估公式。对编辑工作簿的应用这是正确默认,对抽取工作簿的管线这是错误默认。更糟的是,评估不是没有副作用:它把结果写回单元格,翻转脏标志,还会在函数不受支持或外部引用断掉时与产出应用解析得不一样。一个你向运维团队描述为只读的作业,悄悄产出一个与盘上不再一致的工作簿,而之后任何东西保存它,盘上的文件也会变

缓存值读取是契约的另一半。它回答一个更窄的问题,产出应用在这里存了什么?,并拒绝回答任何别的。当你真的要新数字,HotXLS 仍给你依赖图驱动的增量重算;要点是抽取和评估应该是两个不同调用,不是一个调用的两种心情

一个单元格的三个正交事实

先给结论:一个公式缓存值携带三个独立事实,把它们压进单个 Variant 会丢掉你需要的信息。TXLSFormulaCacheInfo 把它们分开存为 StateKindValueTXLSFormulaCacheState 跨五种情形记录出处,即 xlfcsNotFormulaxlfcsMissingxlfcsLoadedxlfcsCalculatedxlfcsInvalidated,而 TXLSFormulaCacheValueKind 把载荷分类为 xlfcvBlankxlfcvNumberxlfcvDateTimexlfcvStringxlfcvBooleanxlfcvError。这种分离让“是否存在”能被诚实报告:缓存的空白、缓存的空串、缓存的 False、缓存的零和缓存的错误都是真实值,于是存在性永远无法从 VarIsEmptyVarIsNull 推断。TryGetCachedFormulaValue 只对 xlfcsLoadedxlfcsCalculated 返回 True,返回 False 时仍填一个可诊断的状态

HotXLS 记录 TXLSFormulaCacheInfo 把一个公式单元格的三个正交事实分开:五种情形的出处 State、六种载荷 Kind、Variant 值 Value,于是缓存空白或 False 绝不会被误当成缓存缺失
出处、载荷类型和载荷值保持分离,这是缓存空白、零、空串或错误能被如实报告为真实值的唯一办法
var
  Book: TXLSXWorkbook;
  Info: TXLSFormulaCacheInfo;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-model.xlsx');
    // SheetIndex、Row、Col 这里都从一起
    if Book.TryGetCachedFormulaValue(1, 12, 5, Info) then
      Writeln('cached value: ', VarToStr(Info.Value))
    else
      Writeln('no usable cache, state ordinal ', Ord(Info.State));
  finally
    Book.Free;
  end;
end;

为什么缓存值缺失?

TryGetCachedFormulaValue 交回 False 恰好有四种原因,状态告诉你是哪种。xlfcsNotFormula 意味着单元格装的是字面量或什么都没有,坐标越界也坍缩成同一答案。xlfcsMissing 意味着单元格真是公式但产出方没为它存值载荷,这是生成器写公式、留给 Excel 首次打开时补结果的常见结局。xlfcsInvalidated 意味着公式文本在加载后被替换,于是曾在那里的值描述的是一个不再存在的表达式。相比之下 xlfcsCalculated 是成功情形:它标记本次会话中你自己的代码或 HotXLS 评估器产出的值,与来自文件的 xlfcsLoaded 相对

对缺失缓存诚实比对它遮遮掩掩更重要。HotXLS 拒绝发明一个值,保存时同样严格:只有 xlfcsLoadedxlfcsCalculated 会发出缓存值,xlfcsMissingxlfcsInvalidated 只写公式,而不是把一个陈旧数字冻进文件。这给你管线里三种正常响应:跳过该行并记录缺口,刻意重算这一个工作簿并接受成本,或评估并对账。如果评估出的数字与产出应用本会写的不一致,公式评估追踪器是找出两个计算在哪里分叉的工具,而不是对着结果猜

跨经典、OOXML 和 ODF 引擎的同一个读取器

管线不该关心刚打开的文件是 BIFF、OOXML 还是 ODF。IXLSFormulaCacheReader 是三者唯一的只读入口:TXLSWorkbook.CreateFormulaCacheReaderTXLSXWorkbook.CreateFormulaCacheReader 都返回一个架在各引擎已在用的稀疏单元格查找上的轻量适配器,表、行、列坐标都是一基且一致。工作簿类刻意不自己实现这个接口,指向工作簿的接口引用会改变其所有权语义,让调用方溜过生命周期租约。相反,销毁工作簿会清掉租约内的原始指针,你的代码还持有的任何读取器在下一次查询时抛 EXLSFormulaCacheReaderInvalidated,而不是解引用已释放内存。这是快速失败的生命周期检查,不是并发保证

var
  Reader: IXLSFormulaCacheReader;
  Info: TXLSFormulaCacheInfo;
  Row, Missing, Errors: Integer;
  Total: Double;
begin
  Reader := Book.CreateFormulaCacheReader;
  Total := 0;
  Missing := 0;
  Errors := 0;
  for Row := 2 to LastRow do
    if Reader.TryGetCachedFormulaValue(1, Row, 7, Info) then
    begin
      case Info.Kind of
        xlfcvNumber: Total := Total + Double(Info.Value);
        xlfcvError:  Inc(Errors);
      end;
    end
    else if Info.State = xlfcsMissing then
      Inc(Missing);
  // 没有计算器运行,没有脏标志移动,Book 未变
end;

缓存字节实际住在哪里

经典 .xls 文件的缓存是 Formula 记录的 FormulaValue 字段,[MS-XLS] §2.5.133 描述的八字节。高字等于 $FFFF 时载荷不是 IEEE 754 双精度而是带标签变体,布局很容易错得微妙:变体类型坐在 val[0],布尔或 BErr 载荷坐在 val[2]val[1] 未定义。HotXLS 之前从 val[1] 读载荷,这类差一只在缓存布尔或错误而非数字的特定文件上浮现。读取器和共享公式写出端现在认同同一组偏移,于是一个缓存的 TRUE 经加载保存原样存活,而不是退化成噪声

HotXLS 读取的经典 XLS Formula 记录八字节 FormulaValue 字段:高字等于 FFFF 时是带标签变体而非 IEEE 754 双精度,变体类型在 val 零,布尔或错误载荷在 val 二
高字为 FFFF 时该字段是带标签变体,载荷在 val[2]、val[1] 未定义,这正是读取器过去误取的那一个字节

包格式里的类型保真是另一组问题,有自己的陷阱。OOXML 中缓存值挂在 c 元素下为 <v>t 属性按 ECMA-376 Part 1 §18.3.1.4 命名类型。HotXLS 把 t="e" 直接读进 varError Variant,保存时映射回标准错误文本,于是错误绝不伪装成普通整数;但 Delphi RTL 在这里帮不了你,因为 VarAsType(Integer, varError) 抛转换异常。可行写法是直接设置 TVarData.VTypeTVarData.VError。日期在相反方向遵循同一纪律:t="d" 和 ODF 日期值类型是显式类型声明,变成 varDate,而 BIFF 数值缓存根本不带日期标志,于是保持 Double。HotXLS 绝不从单元格数字格式猜日期,因为数字格式是呈现而缓存是数据。ODF 还加一个值得知道的情形:office:value-type="void" 表达一个存在但不含值的缓存,而由于 ODF 没有错误值类型,看着像错误的文本按文本保留而不是提升为错误

function DescribeCache(const Info: TXLSFormulaCacheInfo): string;
begin
  case Info.State of
    xlfcsNotFormula:  Result := 'not a formula cell';
    xlfcsMissing:     Result := 'formula stored with no cached value';
    xlfcsInvalidated: Result := 'formula replaced since load';
  else
    case Info.Kind of
      xlfcvError:    Result := 'error code ' + IntToStr(TVarData(Info.Value).VError);
      xlfcvDateTime: Result := DateTimeToStr(VarToDateTime(Info.Value));
      xlfcvBoolean:  Result := BoolToStr(Info.Value, True);
      xlfcvNumber:   Result := FloatToStr(Double(Info.Value));
      xlfcvString:   Result := VarToStr(Info.Value);
    else
      Result := 'present but blank';
    end;
  end;
end;

共享公式共享它们的缓存值吗?

不共享,反过来假设正是一次扫掠最终给整列报同一个数的原理。OOXML 共享公式共享的只是公式表达式和存储优化;每个成员单元格仍拥有自己的 <v>。所以 HotXLS 绝不把根成员缓存传播给没带值到达的跟随者,加载为 xlfcsMissing 的跟随者在保存再打开后仍报 xlfcsMissing。如果你在研究这个组最初如何存储和展开,共享公式 si 属性及其展开的机制另文覆盖;对缓存读取,规则缩成一行:问每个单元格,不信任何你没问来的东西

HotXLS 视角下的 OOXML 共享公式组:si 属性只共享表达式和存储布局,每个成员单元格拥有自己的缓存值,于是无值加载的跟随者持续报告 xlfcsMissing
组共享的是表达式而不是数字,于是根缓存绝不传播,无值到达的成员持续报告那个缺口

缓存值读取、统一的跨引擎读取器、以及你可以选择调用的重算引擎,全部随 Delphi 和 C++Builder 标准版 HotXLS Delphi 电子表格组件交付,不依赖 Excel 或任何 OLE 自动化服务器;产品页载有本文所示工作簿与读取器入口点的完整 API 参考