Magicodes.IE.IO: XML 单元格读写:转义、稀疏列与日期

导出时要把 .NET 值写成合法的 OOXML 单元格;导入时要从不同的单元格表示恢复逻辑列和文本。本文把 XML 转义、UTF-8、Inline String、稀疏列、富文本和日期系统放在一条读写链路里说明。

单元格读写的共同难点是:业务上看到的是"一个值",OOXML 里却有很多表示方式。

写入:先保证 XML 正确,再走快路径

文本节点中,&< 必须转义。当前实现也会保守地处理 >、单双引号和非法控制字符;首尾空白则写出 xml:space="preserve",避免 Excel 改变文本语义。

写入路径会先判断字符串是否是安全 ASCII:

text 复制代码
长 ASCII、无特殊字符
  → 直接写入 UTF-8 字节,可使用向量化转换

短 ASCII
  → 标量复制

中文、特殊字符、非法控制字符
  → UTF-8 编码 / 转义 / 替换路径

SearchValues<char>Ascii.FromUtf16 只是安全 ASCII 路径的实现优化,不是通用 XML 校验器。中文文本通常不需要 XML 实体转义,但它不是 ASCII,仍要经过 UTF-8 编码。

读取:XML 顺序不等于逻辑列顺序

外部 XLSX 可能是稀疏列:

xml 复制代码
<c r="A2"><v>10</v></c>
<c r="C2"><v>30</v></c>

B 列没有节点。Reader 必须从 r="C2" 解析列号,在中间补 null,而不是把第二个 XML 节点当成第二列。

同样,节点可能按 A、C、B 的 document order 出现。Reader 仍应按 cell reference 落到正确的逻辑列。

inline string、富文本与布尔值

inline string 直接位于:

xml 复制代码
<is><t>文本</t></is>

富文本可能有多个 <t>

xml 复制代码
<is><r><t>Hel</t></r><r><t>lo</t></r></is>

Reader 会拼接为 Hello。它恢复的是业务文本,不试图恢复字体和颜色等富文本样式。

布尔单元格通常是 t="b",值 10 会规范成 truefalse,再交给对象映射层转换。

1900 / 1904 日期系统

日期不只看单元格值,还要看 xl/workbook.xml 中的:

xml 复制代码
<workbookPr date1904="1" />

日期处理不能只靠 DateTime.FromOADate。如果工作簿使用 1904 日期系统而读取方忽略该设置,日期会出现固定天数偏移,问题通常很隐蔽。

日期 round-trip 也不应只比较格式化后的文本。Excel 序列号、1900 / 1904 日期系统、DateTimeKindDateTimeOffset 和浮点精度都会影响结果;测试应按组件的序列化语义覆盖这些边界。当前 Reader 会识别 date1904,而新建工作簿的 Writer 固定按 1900 / OA 日期系统写入,尚不支持输出 date1904 工作簿。

一个外部文件场景

外部系统导出的 worksheet 可能同时出现:

text 复制代码
表头在 A、B、C 列
数据行只写 A、C 两列
单元格 XML 顺序是 A、C、B
日期采用 date1904
备注是多个富文本 run

如果 Reader 只是按 XML 节点出现顺序 append,金额会落到错误属性;如果只读第一个 <t>,备注会截断;如果忽略 date1904,日期会整体偏移。Reader 的稀疏列、富文本和日期测试就是针对这些外部工作簿形态,而不是只验证自己生成的最小 XLSX。

导出侧也要用同一组约束反查:特殊字符是否被正确转义、首尾空格是否保留、日期值是否按当前 1900 / OA 写入语义输出。读写能互相 round-trip 只是基础,外部文件仍要单独覆盖。