导出时要把 .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",值 1、0 会规范成 true、false,再交给对象映射层转换。
1900 / 1904 日期系统
日期不只看单元格值,还要看 xl/workbook.xml 中的:
xml
<workbookPr date1904="1" />
日期处理不能只靠 DateTime.FromOADate。如果工作簿使用 1904 日期系统而读取方忽略该设置,日期会出现固定天数偏移,问题通常很隐蔽。
日期 round-trip 也不应只比较格式化后的文本。Excel 序列号、1900 / 1904 日期系统、DateTimeKind、DateTimeOffset 和浮点精度都会影响结果;测试应按组件的序列化语义覆盖这些边界。当前 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 只是基础,外部文件仍要单独覆盖。