一份电子表格带着两层身份。一层是单元格网格,另一层是与之并行的文档元数据:标题、作者、公司、关键字、那些时间戳。Excel 从不在网格中显示这第二层,然而这正是 Windows Search 索引的那一层、SharePoint 据以给文档命名的那一层,以及一个记录管理系统归档时所依据的那一层。当一份生成的工作簿从构建它的模板那里继承了作者和标题,每个下游系统就把模板设计师记录为四千份客户对账单的作者。这些元数据哪里都不正确,却到处都被查阅
HotXLS 在它的两套引擎——用于 .xls 的 BIFF 门面和用于 .xlsx 的 OOXML 门面——上把这一层作为普通的工作簿级属性暴露出来。你在一个文件打开之后读取一个字段,在一个文件保存之前写入一个字段。库决定值落在哪个物理容器里。在写一个生成器之前值得理解的是:每种格式实际支持哪些字段、那些字段物理上住在哪里,以及那条决定一个 .xlsx 是否记录任何元数据的闸门规则
两种格式,两种存储模型
一个电子表格库需要两套元数据实现的原因,以及那些半成品工具把一种格式盖戳正确却忘了另一种的原因,是 .xls 和 .xlsx 把它们的属性放在毫不相干的地方。一个 BIFF 工作簿把它们写进 OLE 复合文件流,主要是那个比 Excel 自身还要早的 SummaryInformation 属性集,旁边是给最后保存文件者命名的流内 WRITEACCESS 记录。一个 OOXML 工作簿把它们作为 XML 部件保存在 zip 包内部,按用途拆分:docProps/core.xml 存放 Dublin Core 字段(标题、创建者、主题、关键字、日期),而 docProps/app.xml 存放公司和生成应用程序等应用级字段,依据 ECMA-376 第 1 部分
HotXLS 把那两种存储模型都展平为工作簿对象的直接属性。你从不手工打开一个属性集流或编辑一个 XML 部件。你给工作簿赋字符串和日期,正确的容器就会在你保存的格式下具现出来
从业务记录给生成的工作簿盖戳
在 XLSX 侧,TXLSXWorkbook 把 Title、Subject、Author、Keywords、Description、Category、LastModifiedBy、Company、Application 和 AppVersion 作为字符串暴露,外加作为 TDateTime 值的 Created 和 Modified,其中零表示未设置。堵住那个继承漏洞的规则只有一句话:每次运行都给每个字段赋值,取值自业务记录,而不是信任模板碰巧携带的东西
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open('statement-template.xlsx') <> 1 then
raise Exception.Create('Template not available');
// 覆盖每个字段:任何未处理的内容都会
// 继承自模板设计者
Book.Title := 'Account Statement 2026-06 / ACME Corp';
Book.Subject := 'Monthly account statement';
Book.Author := 'Billing Service 4.2';
Book.LastModifiedBy := 'Billing Service 4.2';
Book.Company := 'Northwind Financial';
Book.Category := 'Customer Delivery';
Book.Keywords := 'statement;billing;2026-06;acct-10024';
Book.Description := 'Generated document - manual edits are not retained';
Book.Created := Now;
Book.Modified := Now;
Book.SaveAs('statement-10024.xlsx');
finally
Book.Free;
end;
end;
Keywords 字段值得比它通常得到的更多思量。搜索基础设施原样索引它——Windows Search、SharePoint 以及大多数 DMS 产品都一样——所以一个携带账号和期间、以分号分隔的约定,让每份交付的工作簿无需一次数据库往返就成为一条可查找的记录。同样的触达范围也是陷阱所在。属性随文件的每一份副本出行,远过写入它的系统的访问控制,所以个人数据不属于那里
这对时间戳承载着值得在策略里固定而非留给习惯的语义。Created 应标记你的流水线生成文档的那一刻,然后保持冻结。Modified 是接收者每次保存文件时 Excel 会更新的字段,所以交付后两者之间的发散是某人下游编辑过工作簿的正面证据,这能了结不止一场关于一份转发出去的电子表格到底持有谁的数字的争议。一个陷阱藏在未设置状态里:它是字面值零,不是异常也不是 null,所以审计代码必须显式测试零。不加这道守卫就去格式化一个未设置的 TDateTime,你的日志就会充满自信却错误的 1899 年 12 月日期
DocPropsTouched:出厂时不带 docProps 的工作簿
一个只读标志 DocPropsTouched 闸住 XLSX 属性写入器。一个从未给任何属性赋过值的工作簿根本不产生任何 docProps 部件;HotXLS 拒绝写一个空的元数据骨架。这种行为是整洁的,而它有两个值得在设计时围绕的后果
消费侧的入口代码不得假设 core.xml 存在于每个包中。一个硬性要求它的工具会拒绝完全有效的最小文件。而如果你的合规姿态要求每份出站文档至少携带一个生成器身份,那个要求就变成代码而非格式的一项属性:在保存路径中无条件地赋值 Application 和 Author,因为一个未触碰过的工作簿在规范下完全合法,却悄悄违反了你的策略
遗留的 XLS 表面与 Comments 陷阱
BIFF 门面携带那个更老、更小的字段集:Title、Subject、Author、Keywords、Comments、Company 和 Manager,外加 LastSavedBy(UserName 的一个别名),后者写入当另一个用户锁定文件时 Excel 显示的 WRITEACCESS 记录
var
Legacy: IXLSWorkbook; // reference-counted interface: no manual Free
begin
Legacy := TXLSWorkbook.Create;
if Legacy.Open('archive-1999.xls') <= 0 then
raise Exception.Create('Cannot open archive file');
Legacy.Title := 'FY1999 ledger (migrated copy)';
Legacy.Author := 'Archive Migration Batch';
Legacy.Company := 'Northwind Financial';
Legacy.Comments := 'Migrated 2026-06-11; source retained in cold storage';
Legacy.LastSavedBy := 'migration-svc'; // BIFF WRITEACCESS 记录
Legacy.SaveAs('archive-1999-stamped.xls');
end;
一处命名碰撞造成了反复的混淆。这里的文档级 Comments 属性是文件属性对话框中显示的自由文本备注。它与单元格批注毫无关系,后者是通过一套完全独立的 API 附加到区域上的绘图层对象。一场接受了"我们已经写入 Comments"却没检查指的是哪一个的代码评审,就接受了一项关于错误特性的声明,而这发生的频率比这个共享名字所暗示的要高。两者共享四个字母,却不共享一个字节的存储
入口处读取元数据,以及探测缺口
读取是对称的。在 Open 之后,同样的属性从文件中填充回来,这把对传入工作簿的元数据审计变成一个短循环
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open(FileName) = 1 then
begin
Writeln(Format('%s | title="%s" author="%s" created=%s',
[ExtractFileName(FileName), Book.Title, Book.Author,
FormatDateTime('yyyy-mm-dd', Book.Created)]));
if Book.Created = 0 then
Writeln(' no creation date recorded');
end;
finally
Book.Free;
end;
end;
在此期间要围绕一项限制做规划。没有仅属性的探测。GetSheetNames 可以在不加载工作簿的情况下列举工作表,但读取 Title 或 Author 意味着一次完整的 Open,所以对一个大型存档做元数据分诊要在每个文件上付出完整的解析成本。在 BIFF 侧,你可以通过在打开前把 _DisableGraphics 设为 true 来为只读审计削减那份成本,它会完全跳过绘图层。它适合一个只读属性和单元格统计的循环,但在同一个实例可能保存的那一刻就是完全错误的,因为被跳过的绘图内容会被丢掉。当工作表结构本身能预过滤集合时——单工作表导出显然是要跳过的——我们关于工作表列举与轻量级检查的文章中的廉价技术能削减到达昂贵那一遍的文件数。而在批量盖戳任务中——写入数千份输出而不是检查它们——我们关于批处理流式写入的文章中的写入侧吞吐模式原样适用,因为属性赋值给保存时间增加不了什么可度量的东西
跨格式与遏制泄露
属性在单一门面内干净地往返:打开一个 .xlsx、编辑它、保存它,这组属性完好地回来。跨格式是均等性假设破裂的地方,因为 BIFF 和 OOXML 的字段集并不一对一地对齐。BIFF 有 Manager 而没有时间戳;OOXML 有 Category、Description 以及 Created/Modified 这一对。一个盲目复制的转换器会丢失目标格式无法持有的任何东西,所以显式地映射字段,并把映射放进你的转换清单,紧挨着其他所有挺不过不了这趟旅程的东西
模板继承所打开的那道泄露朝另一个方向跑:你从没打算送出去的信息。作者名、停在关键字里的内部项目标签、一个谁也没审批过的草稿标题。上面生成器中的"覆盖一切"纪律就是全部防御,而且值得像一个外人那样去校验——通过打开任何客户都能到达的属性对话框,或者解压 .xlsx 直接从包里读 docProps/core.xml。你在那里看到的正是下游每个索引器看到的
那种下游可见性也是少数几个字段值得比其余更多关切的原因。标题、作者、关键字(作为 Tags 浮现)以及 Comments 或 Description 承载了 SharePoint 和 Windows Search 中大部分的索引权重。一个真正逐文档有别的标题,携带期间和账号,对可查找性的贡献超过任何堆叠在上面的文件夹命名方案,而它每次保存只花费一次赋值
文档属性是一份生成工作簿能携带的最廉价的专业润色,也是没人拥有它们时最常出厂的缺陷。此处描述的两套属性表面都属于 HotXLS Delphi Component,它为 XLS 和 XLSX 原生写入它们,无需 Excel 自动化