Skip to main content

流式 xlsx 读取器,基于 quick-xml + zip 实现真正的流式解析

Project description

stream_xlsx

流式 xlsx 读取器,支持 Rust 库、CLI 工具和 Python 绑定。基于 quick-xml + zlib-rs 实现真正的流式解析,不一次性将整张表载入内存

特点

  • 两种解析模式:
    • default 模式 — 单线程流式,内存最低(~2 GB,适合超大数据集)
    • fast 模式 (--fast) — min(8, cores/2) worker 并发解析,速度 ~3x faster(~4.8s vs ~15s)
  • 流式产出:逐 batch 产出 Polars DataFrame,100 万行 × 60 列(~660 MB)默认模式 ~15s,fast 模式 ~4.8s
  • 多 sheet 支持:打开后可查看所有 sheet 名称,按需切换,sharedStrings/styles 只解析一次
  • 惰性加载:open() 仅解析 sheet 列表;sharedStrings.xml / styles.xml 在首次读取时才加载
  • 类型推断:边读边推断列类型(Int → Float → String),空值不参与推断
  • skip_rows:支持跳过指定 0-based 行索引,不影响 header 解析
  • 日期支持:读取 xl/styles.xmlcellXfs + 自定义 numFmt,自动识别日期列
  • Shell 补全:内置 zsh / bash 自动补全生成

项目结构

sxlsx/              # CLI 工具(cargo build)
stream_xlsx/        # 纯 Rust 库(rlib)
stream_xlsx_py/     # pyo3 Python 绑定(maturin build)

安装

CLI

cargo build --release
# 二进制位于 target/release/sxlsx

Python

cd stream_xlsx_py
maturin build --release
pip install target/wheels/stream_xlsx_py-*.whl

使用

CLI

# 导出为 CSV(default 模式)
sxlsx tf csv data.xlsx --output out.csv

# 导出为 parquet,启用 fast 并发模式
sxlsx tf csv data.xlsx --fast --output out.csv

# 指定 sheet(按名称或索引)
sxlsx tf csv data.xlsx --sheet-name "Sheet1"
sxlsx tf csv data.xlsx --sheet-idx 0

# 统计行数(性能基准)
sxlsx test count data.xlsx
sxlsx test count data.xlsx --fast

# 生成测试文件:100 万行 × 60 列
sxlsx test test-file big.xlsx --rows 1000000 --col 60

# Shell 自动补全
sxlsx completion

fast 模式调优参数

--fast 启用并发解析,以下参数可微调(都有合理默认值,通常无需修改):

sxlsx tf csv data.xlsx --fast \
  --fast-parallelism 8 \     # worker 线程数
  --fast-chunk-size 1000 \   # 每 chunk cell 数
  --fast-queue-cap 1 \       # 队列容量倍数(queue = threads × mul + 1)
  --fast-temp-kb 1024 \      # 解压临时缓冲区(KB)
  --fast-buf-kb 1024         # ZIP 读取 BufReader 大小(KB)

Python

import stream_xlsx_py as sx

# 流式读取(默认 batch_size=10000)
reader = sx.read_xlsx("data.xlsx", batch_size=10000)
for df in reader:
    print(df.shape)

# 查看所有 sheet
print(reader.sheet_names())        # ["Sheet1", "Sheet2"]

# 切换 sheet
reader.select_sheet("Sheet2")
for df in reader:
    print(df.shape)

# 跳过指定行(0-based,不影响 header)
reader = sx.read_xlsx("data.xlsx", skip_rows=[1, 3, 5])
for df in reader:
    print(df.shape)

Benchmark

测试文件:test_100w_60c.xlsx(100 万行 × 60 列,通过 sxlsx test test-file --rows 1000000 --col 60 生成,约 659 MB,59,500,954 个 cell)

CLI 基准通过 sxlsx test count 测试,以 psutil 采样式记录 RSS;Python 基准在 Python 进程内对 stream_xlsx_pypolars.read_excel 进行对比。所有构建均为 Rust release(opt-level=3, lto=fat, codegen-units=1)。

下面分别给出两套环境的结果,再在末尾做横向对比。

macOS — Apple M5

测试环境:macOS,CPU 为 Apple M5(性能核 + 能效核,统一内存架构),Rust release。

Elapsed Time & Peak Memory Comparison

batch_size default 时间 fast 时间 加速 default 内存 fast 内存
1,000 15.10 s 4.82 s 3.1x 2,009 MB 2,685 MB
5,000 15.02 s 4.95 s 3.0x 2,012 MB 2,689 MB
10,000 15.28 s 4.84 s 3.2x 2,016 MB 2,692 MB
50,000 15.51 s 4.87 s 3.2x 2,050 MB 2,728 MB
100,000 15.15 s 4.77 s 3.2x 2,093 MB 2,772 MB
1,000,000 15.22 s 5.53 s 2.8x 2,867 MB 3,545 MB

关键观察:

  • 时间:fast 模式在所有 batch_size 下都 ~3x faster,且对 batch_size 不敏感——不像 default 模式那样需要挑 batch_size
  • 内存:fast 模式多消耗 ~30% 内存(并发 worker 的中间 buffer 和 channel queue),换来 3x 速度

内存曲线

Memory grid — all batch sizes

各 batch_size 下的 RSS 时间曲线,可以看到 default 和 fast 模式的内存稳定在一个平台上,没有持续增长——流式特性的体现。

具体某 batch_size(bs=10000 为推荐配置):

Memory curve — batch=10000

分模式查看:

default fast
default mode memory fast mode memory

Ubuntu — Intel i7-12700KF

测试环境:Ubuntu (Linux 6.17),CPU 为 Intel Core i7-12700KF(8 性能核 + 4 能效核,共 20 线程,DDR4 内存),CLI 模式(./sxlsx test count),Rust release。

Ubuntu — Elapsed Time & Peak Memory Comparison

batch_size default 时间 fast 时间 加速 default 内存 fast 内存
1,000 19.46 s 5.87 s 3.3x 1,651 MB 1,659 MB
5,000 19.62 s 5.82 s 3.4x 1,652 MB 1,658 MB
10,000 19.42 s 5.82 s 3.3x 1,656 MB 1,663 MB
50,000 19.62 s 5.82 s 3.4x 1,691 MB 1,698 MB
100,000 19.91 s 5.87 s 3.4x 1,734 MB 1,743 MB
1,000,000 19.57 s 6.47 s 3.0x 2,523 MB 2,526 MB

内存曲线(bs=10000 为推荐配置):

Ubuntu — Memory curve, batch=10000

default fast
Ubuntu default mode memory Ubuntu fast mode memory

📦 原始数据:benchmark_results.csvbenchmark_timeseries.json;完整内存曲线见 benchmark_plots/

关键观察(Ubuntu 视角):

  • 绝对时间:default 模式 ~19.5s、fast 模式 ~5.9s,fast 模式 3.0–3.4x 加速比,加速比和 Mac M5 上的 ~3x 处于同一量级
  • fast 模式时间更接近 Mac M5:Ubuntu fast 模式 5.8–5.9s,Mac M5 fast 模式 4.8–4.9s,差距仅 ~1s;default 模式差距更大(Ubuntu ~19.5s vs Mac ~15.1s),说明 default 模式对单核 / 内存带宽更敏感,而 fast 模式的并发能把 i7-12700KF 的多核尽量拉满
  • 内存优势明显:Ubuntu 上 default 模式最低 1,651 MB,fast 模式最低 1,658 MB,比 Mac M5 的 2,009 / 2,685 MB 低 20–40%——这是 DDR4 不分页 / 不压缩、统一内存没有显式拷贝开销等因素的综合结果

跨平台对比:Mac M5 vs Ubuntu i7-12700KF

把两套环境的关键 batch_size 放在一起,fast 模式加速比、内存占用:

batch_size Mac M5 default Ubuntu default Mac M5 fast Ubuntu fast Mac 加速 Ubuntu 加速 Mac fast 内存 Ubuntu fast 内存
10,000 15.28 s 19.42 s 4.84 s 5.82 s 3.2x 3.3x 2,692 MB 1,663 MB
50,000 15.51 s 19.62 s 4.87 s 5.82 s 3.2x 3.4x 2,728 MB 1,698 MB
100,000 15.15 s 19.91 s 4.77 s 5.87 s 3.2x 3.4x 2,772 MB 1,743 MB
1,000,000 15.22 s 19.57 s 5.53 s 6.47 s 2.8x 3.0x 3,545 MB 2,526 MB

关键观察:

  • fast 模式加速比跨平台接近:Mac 2.8–3.2x、Ubuntu 3.0–3.4x,差异在 0.2x 以内,说明并发切分 + worker 池的设计在两种 CPU 架构上都能稳定给出 ~3x
  • Mac M5 在 default 模式下更快(15.1s vs 19.5s,约 1.3x):M5 的内存子系统 + 单核 IPC 在 default 这种单线程路径上有优势
  • Ubuntu i7-12700KF 内存占用全面低于 Mac M5:fast 模式 1.6–2.5 GB vs 2.7–3.5 GB,default 模式 1.65–2.5 GB vs 2.0–2.9 GB,差距在 15–45%
  • i7-12700KF 有 20 线程,fast 模式仍然只跑 8 个 worker(min(8, cores/2)=8),所以还没吃满所有核;Mac M5 性能核 4–6 个,min(8, cores/2) 通常也是 4–6,worker 数本身和 M5 性能核更匹配,这也是为什么 fast 模式绝对时间两平台接近的原因之一

Python 环境对比

在 Python 进程中对比 stream_xlsx_pypolars.read_excel:

方案 时间 峰值内存 备注
stream_xlsx_py fast (bs=10000) 4.77 s 2,746 MB 推荐:fast + 流式
stream_xlsx_py fast (bs=50000) 5.09 s 2,817 MB fast + 流式
stream_xlsx_py fast (bs=100000) 5.05 s 2,907 MB fast + 流式
stream_xlsx_py fast (bs=1000000) 5.73 s 3,599 MB fast + 全量
stream_xlsx_py (bs=10000) 14.48 s 2,068 MB default + 流式
stream_xlsx_py (bs=50000) 14.40 s 2,143 MB default + 流式
stream_xlsx_py (bs=100000) 14.59 s 2,907 MB ⚠ default + 流式
stream_xlsx_py (bs=1000000) 14.57 s 3,586 MB ⚠ default + 全量
polars + calamine 23.50 s 11,109 MB pl.read_excel(engine="calamine")
polars + xlsx2csv 92.64 s 10,902 MB pl.read_excel(engine="xlsx2csv")

结论:

  • fast 模式比 default 快 ~3x(14.5s → 4.8s),内存多 ~30% — 与 CLI 行为一致
  • stream_xlsx_py (default) 比 polars+calamine 快 1.6x,内存 低 81%
  • stream_xlsx_py fastpolars+calamine 快 4.9x,内存 低 75%
  • stream_xlsx_py fastpolars+xlsx2csv 快 19x,内存 低 75%
  • 内存曲线(注意 polars 峰值 11 GB vs stream_xlsx_py 稳定 2-3.6 GB):

Python memory usage over time

⚠ default 模式 bs=100000 / bs=1000000 的内存读数偏高(分别 2,907 / 3,586 MB, 较历史正常水平 ~2,200 / ~3,000 MB 高出 600+ / 200+ MB),疑似单次采样的 RSS 抖动/Polars 缓存预热峰值,默认推荐配置(bs=10000, 2,068 MB)未受影响。

推荐配置

场景 推荐 理由
超大文件(>100 MB)、内存敏感 default + batch_size=10000 内存最低(~2 GB),无并发开销
通用大文件、追求速度 fast + batch_size=10000 ~3x faster,内存多 30%
全量加载到单个 DataFrame fast + batch_size=1000000 一次产出,无 batch 调度
Python 通用 stream_xlsx_py 默认参数 已经过调优,无需额外配置
Python 性能优先 stream_xlsx_py + fast=True ~3x 加速,内存代价 ~30%

工作原理

流式解析管线(default 模式)

ZIP file (deflate)
   │  后台线程逐 chunk 解压
   ▼
mpsc channel (256 KB chunks)
   │
   ▼
quick-xml 事件循环
   │  边解析边产出 Cell<Data>
   ▼
TypedCols (按类型 zero-copy 存储)
   │
   ▼ ① Int64/Float64/Bool/DateTime: from_vec_validity
   ▼ ② String: PlSmallStr → MutablePlString → StringView(零拷贝封装)
   Polars DataFrame

fast 模式:并发解析

fast 模式在 default 模式基础上,加上 min(8, cores/2) worker 线程并发解析单元格 (运行时通过 std::thread::available_parallelism() 取核心数,8 和 cores/2 取较小值,可通过 --fast-parallelism 显式覆盖):

ZIP file
   │  后台解压线程
   ▼
单线程 scanner (split boundaries + memchr)
   │  按 CHUNK_SIZE (默认 1000 cells) 切分
   ▼
crossbeam channel
   │  queue capacity = parallelism × 1 + 1 (天然背压)
   ▼
┌──────────┬──────────┬───── ┬──────────┐
│ worker 1 │ worker 2 │ ... │ worker N │   (N = min(8, cores/2))
└──────────┴──────────┴───── ┴──────────┘
   │  并发解析各 cell 字节
   ▼
result channel
   │
   ▼
main thread (按 seq 顺序重组 + 输出 DataFrame)

关键技术点:

  • scanner 与解压并行:scanner 单线程扫 XML 边界,worker 解析纯 CPU 任务
  • CHUNK_SIZE=1000:每个 chunk 包含 1000 cells,在 channel 往返开销和 worker 利用率之间取得平衡
  • 天然背压:queue 容量 = parallelism + 1,worker 满载时 scanner 自动阻塞

已知瓶颈:chunk 切分

实测将 --fast-parallelism 从 1 调到 8,fast 模式的吞吐变化很小(~3x 加速已基本吃尽 worker 池能贡献的上限,继续堆 worker 不再变快),说明瓶颈不在 worker 侧,而在 单线程 scanner 的 chunk 切分

曾尝试用 memchr 加速边界扫描,实际比当前实现还慢,推测原因是:

  • 切分过程中需要把每个 chunk 区间 to_vec 拷贝成独立 buffer 交给 worker
  • 大文件下切分次数极多(每 1000 cells 一次),频繁的小块分配/拷贝是主要开销

这意味着两点:

  • 当前 fast 模式已基本贴近"切分 + 解析"两阶段的理论上限,再要提速必须 解决切分侧分配问题——例如 zero-copy Bytes / BytesMut 切片,或把切分逻辑 移进 worker 让解压直接落到 per-worker buffer
  • sharedStrings 也是边解压边解析的同类管线——一旦切分提速,共享字符串的 fast 路径可以同步受益,因为它和 sheet XML 共用同一套切分 / 派发机制

有兴趣的同学可以从 stream_xlsx/src/sheet_fast.rs 的 scanner 入手,尝试用 bytes::Bytes / bytes::BytesMut 替换 to_vec 路径。

零拷贝 DataFrame 构建

数字列(Int64 / Float64 / Bool / DateTime):

  • 边读边存入 Vec<T> + MutableBitmap
  • 通过 from_vec_validity 零拷贝转为 Arrow 数组
  • 避免 AnyValue 24 字节/值的开销

字符串列(SharedString / InlineString / DateTimeIso / DurationIso):

  • PlSmallStr 内部存储:长度 ≤22 字节直接 inline,无需堆分配
  • MutablePlString::push_value + freeze() 封装为 Utf8ViewArray,只标记所有权,不复制
  • DataFrame::from_chunks 零拷贝持有 Arrow 数组

局限性与已知问题

标题行仅支持单行

has_header=True 时,只把第一行作为列名。多级表头(例如合并单元格跨越两行形成"分类 + 字段"两层结构)目前不支持——会被当成数据行处理,导致第一行表头被合并到字符串列里。skip_rows 参数用于跳过数据行,对多级表头无帮助。

如需处理多级表头,可在读取后用 Polars 自行重塑列名,或预处理 xlsx 把多级表头合并为一行。

fast 模式共享字符串解析是简化版

为追求速度,fast 模式只识别最简单的 <si><t>...</t></si> 模式。遇到以下情况会回退到 default 模式的 streaming quick-xml 解析器:

XML 模式 示例 fast 模式行为
富文本(rich text) <si><r><t>bold</t></r><r><t>normal</t></r></si> ❌ 回退
CDATA 区段 <si><t><![CDATA[<value>]]></t></si> ❌ 回退
XML 实体 <si><t>a &amp; b</t></si> ❌ 回退
<t> 节点 <si><t>part1</t><t>part2</t></si> ❌ 回退
简单纯文本 <si><t>hello</t></si> ✅ 走快路径

实际影响:绝大多数由 Excel / WPS / Google Sheets 生成的 xlsx 文件用 simple 模式,fast 路径可覆盖 99%+ 场景。回退到 slow path 仅在共享字符串表本身较慢,不影响 95% 时间的 sheet XML 解析(那才是 fast 模式的真正优化点)。

如确需 fast 模式处理上述复杂情况,可手动将 xlsx 的 sharedStrings.xml "扁平化"(用 LibreOffice 重新保存通常就够了)。

构建

开发构建

# Rust 库 + CLI
cargo build --release

# 测试
cargo test --workspace

# Python wheel
cd stream_xlsx_py
maturin develop   # 开发模式,直接链接到 .venv
maturin build --release

CI

项目已配置 GitHub Actions(.github/workflows/),每次 push/PR 自动运行:

  1. cargo test --workspace
  2. cargo build --release(CLI artifact)
  3. maturin build --release(多平台 wheel artifact)

支持平台:Linux x64/ARM64、Windows x64/ARM64、macOS x64/ARM64。

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

stream_xlsx_py-0.3.2-cp39-abi3-win_arm64.whl (4.6 MB view details)

Uploaded CPython 3.9+Windows ARM64

stream_xlsx_py-0.3.2-cp39-abi3-win_amd64.whl (5.4 MB view details)

Uploaded CPython 3.9+Windows x86-64

stream_xlsx_py-0.3.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (5.9 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ x86-64

stream_xlsx_py-0.3.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (5.4 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ ARM64

stream_xlsx_py-0.3.2-cp39-abi3-macosx_11_0_arm64.whl (5.1 MB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

stream_xlsx_py-0.3.2-cp39-abi3-macosx_10_12_x86_64.whl (5.4 MB view details)

Uploaded CPython 3.9+macOS 10.12+ x86-64

File details

Details for the file stream_xlsx_py-0.3.2-cp39-abi3-win_arm64.whl.

File metadata

File hashes

Hashes for stream_xlsx_py-0.3.2-cp39-abi3-win_arm64.whl
Algorithm Hash digest
SHA256 cdf1a4bb229b739ddf1f029e71a995121d06849aef98a4bf5eee9292d04330ae
MD5 db2ad586844f47c8dcb57a7cf18d36b2
BLAKE2b-256 d0344e369c3a39b576b8fc4d7ad46c24bf3fda56c2e574958b40f4ff9dccf8b6

See more details on using hashes here.

Provenance

The following attestation bundles were made for stream_xlsx_py-0.3.2-cp39-abi3-win_arm64.whl:

Publisher: ci.yaml on LittleTomatoPotato/Stream-xlsx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file stream_xlsx_py-0.3.2-cp39-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for stream_xlsx_py-0.3.2-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 f0436f2cfbe383e08e928c0bff316f75492d9e52c00c831b0bc07672271d05f0
MD5 079d86ede2a472696074f7b5f7fe5a93
BLAKE2b-256 11e3c1cdc812e3da3cb9c0f5c20970468f4066d75c44f717b86cc280159d1d5c

See more details on using hashes here.

Provenance

The following attestation bundles were made for stream_xlsx_py-0.3.2-cp39-abi3-win_amd64.whl:

Publisher: ci.yaml on LittleTomatoPotato/Stream-xlsx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file stream_xlsx_py-0.3.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for stream_xlsx_py-0.3.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 c8d588009f6186bca181de5ef0396d21bb114c627e88eb59f5441c0b71db94d9
MD5 e71c0ed3d5ba151109cebfdd1a3b734b
BLAKE2b-256 8753880624209a1c60240af8dde40c4ed8d79082b4010647091ff008e6ab99a6

See more details on using hashes here.

Provenance

The following attestation bundles were made for stream_xlsx_py-0.3.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: ci.yaml on LittleTomatoPotato/Stream-xlsx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file stream_xlsx_py-0.3.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for stream_xlsx_py-0.3.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 05064367e0bd8b6ea11d44f979ccb74d79bf35a65309c5b5946d907111c728ab
MD5 48431fd11330923772bc16145428052f
BLAKE2b-256 14ce68f3556b6312ad5733b694dc7731b79b17cf7fab72c70da2df1ef4f3785a

See more details on using hashes here.

Provenance

The following attestation bundles were made for stream_xlsx_py-0.3.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: ci.yaml on LittleTomatoPotato/Stream-xlsx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file stream_xlsx_py-0.3.2-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for stream_xlsx_py-0.3.2-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 1234c13d503ade1effab0274311814378681809053b999a03135ecb5fe0b3f22
MD5 ad9aee6edb28338f8715fff26af62cb2
BLAKE2b-256 694980cbb702d9937c02361607d048dfd66bef1959eec91a5cc4c6438b2fc3f7

See more details on using hashes here.

Provenance

The following attestation bundles were made for stream_xlsx_py-0.3.2-cp39-abi3-macosx_11_0_arm64.whl:

Publisher: ci.yaml on LittleTomatoPotato/Stream-xlsx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file stream_xlsx_py-0.3.2-cp39-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for stream_xlsx_py-0.3.2-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 72a1662327c330767c0441b858f0397c3ef53c023dd513a2750951e492341048
MD5 8d3329733221ccc8cc947ed640952675
BLAKE2b-256 089236892b5d3265eb230a9bcce209504be4cbb41eb355f1bf9ed98ed4b9d514

See more details on using hashes here.

Provenance

The following attestation bundles were made for stream_xlsx_py-0.3.2-cp39-abi3-macosx_10_12_x86_64.whl:

Publisher: ci.yaml on LittleTomatoPotato/Stream-xlsx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page