Magicodes.IE.IO:XLSX I/O 管线:数据如何进出工作簿
导出时,数据对象怎样变成 XML 和 ZIP;导入时,ZIP、关系文件和 XML 行又怎样变回对象?本文只建立读写管线的整体视图,缓冲、ZIP、单元格、SST、映射、异步和验证留到后续文章展开。
Magicodes.IE.IO 处理的是 XLSX 的按行读写,不是完整 Excel 对象模型。它不负责让调用方任意修改已有工作簿、计算公式或恢复所有 Excel 显示语义;它的核心目标是让业务对象和 XLSX 行数据之间可以顺序转换。
这一区分很重要。把组件称为“Excel 引擎”很容易让读者期待一个可随机编辑、可计算公式、可保留全部工作簿状态的对象模型。本文讨论的是另一类问题:在文件、HTTP 响应或上传流上,怎样可靠地读写 XLSX 数据。
导出:对象怎样变成 ZIP 输出
高层入口可以很简单:
// Foo 与 foos 的定义见第 00 篇。
Xlsx.Write("foo.xlsx", foos);
await Xlsx.WriteAsync(response.Body, asyncFoos, cancellationToken: ct);
内部写入路径大致如下:
IEnumerable<T> / IAsyncEnumerable<T>
↓
ExportProfile<T>
↓ 冻结配置,构建 RowPlan
列元数据 / getter / cell writer / 样式
↓
XlsxWriter
↓ 逐行生成 worksheet XML
ByteBufferWriter
↓ 局部 XML 字节缓冲
ForwardOnlyZipWriter
↓ Local Header / 数据 / Data Descriptor
Stream / IBufferWriter<byte> / 文件
这里有两类状态:
逐行状态
当前 row、当前 cell、局部 XML 缓冲
工作簿状态
styles、sheet 信息、SST、Central Directory 元数据
逐行状态可以持续推进;工作簿状态要到完成时才能写出全部内容。比如启用 SST 时,唯一字符串表必须保存到结束;ZIP 的 Central Directory 也要等所有 entry 完成后写入。
导入:ZIP 怎样变回对象
读取入口对应另一条方向:
// Foo 的定义见第 00 篇。
foreach (var foo in Xlsx.Read<Foo>(stream))
{
// 按行消费
}
await foreach (var foo in Xlsx.ReadAsync<Foo>(stream, cancellationToken: ct))
{
// 异步按行消费
}
Reader 的主路径是:
Stream
↓
ZipArchive
├─ workbook.xml / workbook.xml.rels
├─ date1904
└─ sharedStrings.xml(如存在)
↓
第一个可读 worksheet XML
↓ XmlReader 逐行读取
稀疏列 / inline string / SST / 富文本
↓
XlsxReadPipeline
表头 / 显式映射 / Converter / Source Generator
↓
T
导入同样不是“整个文件都读入内存”。工作表 XML 行可以逐行消费,但 Shared String Table 会整体加载;输入流、ZIP 解压、当前行缓冲和业务对象也都占用内存。
读写共享的结构约束
导入和导出并不是两套无关代码,它们都受同一份 OOXML 结构约束:
| 结构 | 导出侧 | 导入侧 |
|---|---|---|
| 工作簿关系(workbook relationship) | 写 workbook 与 sheet 的关系 | 通过关系定位第一个 worksheet |
| 单元格引用 | 写 A1、B1 等位置 |
由引用恢复稀疏列位置 |
| inline string / SST | 选择字符串存储策略 | 还原字符串文本 |
| 日期系统 | 写 Excel 日期值与格式 | 读取 date1904 后解析日期 |
| ZIP entry | 顺序写 Data Descriptor | 打开 entry 并读 XML |
| 对象映射 | 属性写成列 | 表头和列写回属性 |
这也是本文后续按“包结构、单元格、字符串、映射、异步”来组织,而不是把导入和导出拆成两套薄文章的原因。
低分配的边界
写入侧的热点路径会尽量减少临时数组和字符串:池化字节缓冲、UTF-8 直接格式化、固定 XML 片段和生成的 cell writer 都服务于这个目标。
但“低分配”不是整条 I/O 管线的通行证:
ToBytes必然物化最终byte[];netstandard2.0有兼容分支;- SST 会维护全量唯一字符串表;
- Reader 会加载 sharedStrings;
- 真正异步 I/O 有状态机和底层流成本;
- DTO 映射仍会创建业务对象。
因此文章中的性能结论必须限定到具体 API、TFM、数据形态和输出/输入目标。
流所有权也不同
| API | 默认所有权 |
|---|---|
Xlsx.Write(Stream, ...) |
调用方负责释放输出 stream |
Xlsx.WriteAsync(Stream, ...) |
调用方负责释放输出 stream |
Xlsx.Read(Stream, ...) |
枚举结束时 Reader 默认关闭输入 stream |
Xlsx.ReadAsync(Stream, ...) |
枚举结束时 Reader 默认关闭输入 stream |
读取时如果调用方仍要使用输入流,应传 leaveOpen: true。输出响应开始写入后也不能回滚已发送的 ZIP 字节,这些生命周期问题和性能同样属于 I/O 设计的一部分。
结尾
XLSX I/O 是一条受 ZIP、OOXML、字符串存储、流生命周期和对象映射共同约束的管线。理解这条管线后,后面的优化和边界才有落点。
浙公网安备 33010602011771号