设想一个每夜任务,它在代码中构建一份发票工作簿并写成 CSV 供一个下游系统导入。数字在 Excel 里看起来是对的。CSV 在文本编辑器里干净地打开。然后导入器在总计列上噎住了,因为第 42 行的金额字段读作 =SUM(D2:D41),公式的字面文本,而不是它本该算出的数字。什么都没坏。这是文档化的行为,而它正是要从 HotXLS 导出时首先要理解的事:写入器精确地按单元格模型当前的状态序列化它,而一个值从未被算过的公式单元格只有它的公式文本可以交出
为什么你的 CSV 里是公式而不是数字
HotXLS 把公式文本和算出的值存储为两件分开的事。SaveAsCSV 在出口不跑计算引擎,这是设计使然:导出不应改变工作簿,也不应冒着在一条病态公式链上卡住的风险。Excel 自己保存的文件在公式旁边带着缓存结果,所以重新导出它们的行为如你所料。陷阱专门针对你自己的代码生成的工作簿,那里公式被写入了却从未求值。修复办法是在导出之前让值存在,使用同一个能解析跨表引用和自定义函数的 Calculate 引擎:
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
R: Integer;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('invoice-run.xlsx');
Sheet := Book.Sheets[0];
// 物化公式结果,使 CSV 携带数字而非 '=...' 文本
for R := 2 to 41 do
if Sheet.Cells[R, 4].Formula <> '' then
Sheet.Cells[R, 4].Value := Book.Calculate(Sheet.Cells[R, 4].Formula);
Book.SaveAsCSV('feed.csv', 0, ','); // 第 0 张工作表,逗号
Book.SaveAsCSV('feed.tsv', 0, #9); // 同一张工作表导出为 TSV
finally
Book.Free;
end;
end;
注意这个循环实际做了什么:它用算出的值覆盖公式单元格。对于一次用完即弃的导出这一遍,这完全正确,但如果你打算之后再把工作簿保存为 .xlsx,这就错了,因为你刚刚用冻结的数字替换了活公式。从一个副本导出,或者把写回范围限定为只触碰这次导出运行。Calculate 背后的引擎走得更远,包括注册你自己的函数,这正是HotXLS 公式引擎与自定义函数的主题
分隔符写入器保证什么
CSV 路径产出带字节顺序标记的 UTF-8、CRLF 行尾以及 RFC 4180 引号规则。任何包含分隔符、引号或换行的字段都被包裹,嵌入的引号被翻倍。日期无论单元格的显示格式如何都渲染为 yyyy-mm-dd hh:nn:ss。对于机器消费者这是正确的选择,尽管它会让任何期望屏幕上的格式带过来的人意外。富文本单元格通过拼接其运行被展平
这些默认值在大多数与导入器的争论开始之前就了结了它们,但其中两条仍然属于你的接口契约。第一条是 BOM。正是它让 Excel 能带着完好的重音字符打开文件,然而少数严格的解析器把那三个字节当作数据;如果你的解析器是其中之一,在交接时剥掉它们。第二条是 TSV。它根本不是一项独立功能,只是同一个写入器以 #9 作为分隔符调用而已,所以上面的一切对它原样适用。要导出的工作表在多参数重载中按 0 基索引选择,而单参数的 SaveAsCSV(FileName) 简写取活动工作表
HTML 导出是快照,不是交换格式
CSV 丢掉除值之外的一切,而 SaveAsHTML 试图保留外观:每张工作表一个 <table>,合并区域以 colspan 和 rowspan 表达,基础单元格样式作为内联 CSS。相对主题的颜色被跳过而非解析,所以一个依赖主题插槽的模板出来比它在 Excel 中看起来更朴素。对任何必须挺过这趟旅程的东西设显式的 RGB 颜色。选项对象控制外壳:
var
Opts: TXLSXHtmlExportOptions;
begin
Opts := TXLSXHtmlExportOptions.Create;
try
Opts.Title := 'Weekly settlement';
Opts.TableClass := 'report-grid'; // 宿主页样式表的钩子
Opts.WriteDocument := True; // 整页,而非片段
if Book.SaveAsHTML('settlement.html', 0, Opts) <> 0 then
raise Exception.Create('Sheet index out of range');
finally
Opts.Free;
end;
end;
那段代码片段里有两处细节值得留意。把 WriteDocument 翻为 False,输出就变成一段裸表片段而非整页,这正是你想把预览注入一个已有布局时所要的:设 TableClass 并让宿主页样式表来做主题化。返回约定也与大多数 HotXLS 调用相反。SaveAsHTML 成功返回 0,工作表索引错误返回 -1,所以一个习惯驱动的 = 1 检查会把每次成功的导出报告为失败。当你需要的是某个区域而非整张工作表时——也许是为了邮件或嵌入单个块——TXLSXRange.SaveAsHTML 在同样的渲染规则下导出任何矩形区域
RTF 输出以及它仍然有用武之地的地方
第四个目标写入 RTF 1.6 表格,每次调用通过 SaveAsRTF 写一张工作表。列宽大致按每字符列宽约 96 缇来近似。值得了解的结构性限制是合并单元格在输出中不跨列延伸:只有锚点单元格携带其内容,被覆盖的单元格输出为空白。这把 RTF 排除在版面密集的模板之外。它仍然作为把表格结果丢进一个文字处理器或一个早于 HTML 摄取的遗留文档管理系统的阻力最小路径而有用武之地
往返:按设计破坏性地导入 CSV
把 CSV 读回有自己的契约。OpenCSV 清空整个工作簿并把它重建为一张名为 Sheet1 的工作表。它在精神上是一个构造器,不是一次合并,所以绝不要在一个仍持有未保存内容的工作簿上调用它。传 #0 作为分隔符会触发自动分隔符检测。ADetectTypes 标志控制类型提升:开启时,数字字符串变成数字、ISO-8601 字符串变成日期、true/false 变成布尔值。当数据源携带带前导零的标识符、邮政编码或产品代码时把它关掉,提升会悄悄把所有这些都变成数字(一个前导零在 00123 变成 123 的那一刻就没了)。两套门面暴露同样的导入。把它与上面的导出调用配对,你就拥有了一座在流水线中任何地方都不需要 Excel 安装的格式桥——这正是HotXLS 的数据库到 Excel 报告生成所涵盖的场景
直接导出到流
这里的每个写入器在文件名版本旁边都有一个流重载:CSV、HTML、RTF 以及工作簿格式本身。在服务器代码中,那些重载才是该伸手去用的。一个提供 CSV 下载的 Web 端点可以写进一个 TMemoryStream 并把它直接交给响应对象,没有临时文件、没有清理任务、也不会在碰巧挑了同一个生成名的两个请求之间发生冲突。把导出推进 blob 存储或附加到出站邮件时同样如此。文件系统完全从画面中退出
那种模式与库部署方式叠加。两套门面都是原生 Object Pascal 读写器,所以没有 Excel 安装、没有 COM 自动化,也没有在服务器上把请求串行化的每进程瓶颈。每个请求可以拥有自己的工作簿对象,运行第一节中的计算写回,并与它的邻居并行地流式导出。内存是需要盯紧的唯一资源。工作簿模型在导出期间活在 RAM 中,所以一个打开非常大文件只为了把它们重新发成 CSV 的服务应该限制并发任务,或把超大的那些排队,而不是让一次流量高峰来决定工作集
还有一个较小的旋钮:当片段将被保存为一个独立文件、而下游某个工具会嗅探其编码时,在 HTML 选项上设 IncludeBOM。当你直接通过 HTTP 提供 HTML 时,把字符集声明留给响应头
当字节仍然出来是错的时候
关于 CSV 导出最常见的支持问题是开头那个问题换了件外衣:Excel 显示乱码而不是重音字符。本能是责怪写入器,但它正是为此发出一个 UTF-8 BOM,而文件离开你的代码时几乎总是正确的。在那与 Excel 之间的某个东西吃掉了 BOM。一次文本模式的 FTP 传输、一次跳过前三个字节的流复制、一个在穿过时重新编码的代理:其中任何一个都会剥掉那个标记,留下 Excel 去猜测编码,而它猜得很差。在边界处而非导出调用处诊断这个问题。在一个十六进制查看器里打开交付的文件,确认 EF BB BF 仍然是其中的第一样东西
这就是所有四种格式的贯穿主线。导出调用是容易的部分,而 HotXLS 在写入器面对的每个决策上都做出了可辩护的选择。失败活在接缝处——公式文本遇上一个想要数字的解析器、一个 BOM 遇上一个不保留它的传输、一个合并单元格遇上 RTF 的扁平表格模型。其中每一项都是一项要写进你的导出器与消费它者之间契约的事实,因为消费者无法从字节中读出你的意图。关于两套工作簿门面的完整方法列表,HotXLS Delphi Component 产品页带有完整参考