Magicodes.IE.IO:XLSX 包结构:ZIP、关系文件与顺序写入

Magicodes.IE.IO:XLSX 包结构:ZIP、关系文件与顺序写入

XLSX 是一组有关系约束的 ZIP 部件。导出要在不能回写的输出流上写 Local Header、Data Descriptor 和 Central Directory;导入则要从工作簿关系(workbook relationship)找到真正的工作表。本文把两条路径放在同一个包结构里解释。

XLSX 不是“一个 XML 文件”,而是一个 ZIP 包:

[Content_Types].xml
_rels/.rels
xl/workbook.xml
xl/_rels/workbook.xml.rels
xl/worksheets/...
xl/styles.xml
xl/sharedStrings.xml(可选)

导出和导入面对的是同一个包,只是方向相反:

导出:对象 → XML part → ZIP entry → 输出流
导入:输入流 → ZIP entry → XML part → 对象

导出:为什么要支持顺序写

HTTP 响应流通常不支持 Seek。写一个 ZIP entry 时,Local Header 在数据之前,但 CRC、压缩后尺寸和未压缩尺寸要等数据写完才知道。

ZIP 的 Data Descriptor 允许 Local Header 先把这些字段留空:

Local Header(CRC / size = 0)
压缩数据
Data Descriptor(真实 CRC / size)

ForwardOnlyZipWriter 用这个机制写 entry。entry 完成后,再把已经得到的 CRC、尺寸和偏移保存为 Central Directory 元数据;所有 entry 完成后,统一在文件尾写 Central Directory。

ZIP Entry 的数据流

一条 entry 的数据流是:

业务 XML 字节↓
EntryWriteStream├─ 统计未压缩大小├─ 更新未压缩数据的 CRC32↓
ICompressor / DeflateStream↓
CountingWriteStream├─ 统计压缩后大小↓
底层输出流

这不是“任意几层 Stream 都能随便拼”的模式演示。CRC、压缩、计数和 Data Descriptor 共享 entry 生命周期:压缩器必须 Finish,才能得到最终压缩尺寸;Data Descriptor 必须在尺寸和 CRC 确定后写入。

导入:不能假设第一个工作表叫 sheet1.xml

读取时,最简单的实现会直接打开:

xl/worksheets/sheet1.xml

外部工具生成的工作簿不保证这个路径就是逻辑上的第一个 sheet。Reader 当前读取第一个可读 worksheet,定位路径来自:

xl/workbook.xml↓ sheet 的 r:id
xl/_rels/workbook.xml.rels↓ Relationship.Target
xl/worksheets/实际文件.xml

XlsxReader 先根据工作簿关系(workbook relationship)找第一个可读 worksheet;关系文件缺失时,才回退到 worksheets 目录中的 XML entry。这让 Reader 能处理工作表路径不按默认命名的 XLSX。

同一个边界,读写两边都要守

约束 导出侧 导入侧
关系文件 写出 workbook 与各 part 的关系 根据关系定位 worksheet
Shared Strings 最后写出 sharedStrings.xml 初始化时加载 sharedStrings.xml
ZIP 顺序 Data Descriptor + Central Directory ZipArchive 打开现有包
流所有权 Write(Stream, ...) 由调用方释放 Read(Stream, ...) 默认在枚举结束时关闭

ZIP32 的范围

当前 writer 不支持 ZIP64。单个 entry、归档偏移和 Central Directory 尺寸受 ZIP32 的 32 位字段限制;entry 数量和名称长度还有 16 位字段限制。超过限制时应拆分文件、拆分 sheet,或改用支持 ZIP64 的方案。