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
单元格引用 A1B1 等位置 由引用恢复稀疏列位置
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、字符串存储、流生命周期和对象映射共同约束的管线。理解这条管线后,后面的优化和边界才有落点。

posted @ 2026-07-18 21:12  HueiFeng  阅读(7)  评论(0)    收藏  举报