HotXLS 可以在不解析、也不重新压缩文件其余部分的前提下,重写现有 XLSX 包内的一个工作表。TXLSDirectWriter.BeginPatch 打开一个源包,把目标工作表以外的每个条目都以压缩字节原样复制过去,让你通过常规的 AddSheet、AddRow 和 Write* 调用重新编写那一个工作表。图表、数据透视缓存、主题、样式和共享字符串完全不会被解压
这项工作流所解决的问题出现在报表和数据刷新场景中。业务团队交来的工作簿带着数据透视表、切片器、条件格式,以及十年积累下来的格式设置。每天夜里都要把其中一张数据表替换成最新数字。加载并重新保存整个工作簿,每个文件要花上几分钟,更重要的是,还会给那些加载引擎必须重建的特性带来保真度风险。原地修补通过不去碰不需要碰的内容,同时避开了这两个问题
为什么复制压缩字节才是有意思的部分?
一个在压缩层面被复制的 zip 条目只需要一次流拷贝。同一个条目走常规写入路径,则需要在读入时解压、在写出时再压缩,而压缩正是开销较大的那一半。在一个带有大型数据透视缓存和几十张嵌入图片的工作簿上,这种差异,就是一次能在写完新工作表所需的时间内完成的修补,和一次把大部分时间都花在重新压缩它从未检视过的字节上的修补之间的差别
HotXLS 为此使用 CopyCompressedFrom,它把源条目的压缩字节直接写入目标归档。当某个条目无法这样复制时,比如使用了不同的压缩方式或弱加密,写入器会退回到解压后的流拷贝,而不是直接失败。目录标记条目会被跳过,因为写入器会生成自己的目录标记
原地替换,或写入新文件
两个重载方法覆盖了这项任务的两种形态。原地形式会把结果暂存在原文件旁的一个临时文件中,关闭源句柄,然后删除并重命名,因此写入中途崩溃也不会损坏原文件。显式指定目标的形式则完全不动源文件,既可以替换某个工作表,也可以追加一个新表:
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 中的工作表注册表,以及把每张工作表绑定到其部件的关系映射,然后选出下一个空闲的部件编号、工作表标识符和关系标识符。关系类型遵循源包的既有约定,因此修补一个严格遵循 ISO 29500 的工作簿会输出严格版关系类型,修补一个过渡版工作簿则会输出过渡版类型
补丁刻意丢弃和限制了什么
计算链在两种模式下都会被丢弃。在替换模式下,其条目描述的是一张已不复以原有形式存在的工作表中的单元格;在插入模式下,工作表索引的偏移会直接让它失效。Excel 会在下一次重新计算时重建这条链,因此丢弃它是正确的做法,而不是有损的做法。这个部件会从复制中被排除,其关系条目和内容类型覆盖也会被精确移除
补丁内部有两处写入语义发生了变化,两者都遵循同一条原则:补丁不得扰动它没有重写的部件。字符串是直接内联写入工作表的,而不是添加到共享字符串表中,因为源表本身是原样跨过的。而 StyleIndex 指向的是源包的 cellXfs 中的条目,而不是写入器自己构建的样式表。这意味着你可以引用原始工作簿已经定义好的格式,这通常正是数据刷新想要的效果,但也意味着你必须知道哪个索引对应哪种格式
// 在补丁内部,StyleIndex 索引的是源包的 cellXfs。
// 日期需要一个显式的索引,指向那里的某个日期格式:
W.WriteDateTime(3, EncodeDate(2026, 8, 22), DateStyleIndexFromTemplate);
// 无样式的 WriteDateTime 重载在补丁模式下会被拒绝,
// 因为它假定使用写入器自己的样式表,而补丁模式
// 从不创建这样的样式表
有六个写入入口被禁止使用:添加表格、图表、图片、批注、已定义名称和单元格样式,在补丁模式下都会抛出异常,关闭时还有第二道安全网,只要其中任何一项的计数器不为零就会失败。这些特性每一项都需要编辑补丁会原样复制的部件,而一个改了一半的包比一次被拒绝的操作更糟糕。每次操作只能修补恰好一个工作表
何时该用补丁,何时该加载
当工作簿很大、改动只局限于一个工作表、且文件其余部分必须逐字节保持不变时,补丁就是正确的工具。当改动跨越多个工作表、需要新的格式或新对象、或者文件小到普通的加载再保存根本没有成本时,补丁就是错误的工具。对于从零开始的批量生成,流式直接写入器 中描述的方案仍是更合适的选择,它与补丁共享同一套 AddRow 和 Write* API,因此在两者之间切换只是机械性的改动
如果你确实需要完整对象模型,已加载工作簿内部的工作表级操作在 复制 XLSX 包中的工作表 中有介绍。而如果你考虑使用补丁的原因是整份工作簿的处理速度变慢了,那么在选择方案之前,值得先读一读 大型工作簿性能 中的测量数据和内存行为
验证补丁确实达到了预期效果
三项检查几乎能捕获所有错误。确认预期应保留的部件仍在归档中,确认 xl/calcChain.xml 已经消失,并确认通过 TXLSXWorkbook 重新打开文件后报告的工作表数量符合预期:替换模式下不变,插入模式下加一。回读修补后的工作表并核对若干数值和公式,能让整个验证闭环
开发这项功能过程中留下的一条实现细节值得重提,因为它可能咬到任何编写类似 zip 层代码的人。工作表部件名称是按前缀匹配的,前缀长度上差一个字符就会让判定条件永远匹配不上,于是新写入的部件会与既有名称冲突,而那些取同名条目中最后一个的读取器会悄悄选中错误的工作表。如果补丁看起来像是把两个工作表的内容互换了,先检查名称匹配逻辑,再去查 XML
原地修补、流式写入和完整的工作簿对象模型都封装在适用于 Delphi 和 C++Builder 的同一个库中;功能列表见 HotXLS Delphi 电子表格组件页面