#excel #xlsx #file-reader #ods #xls #read-memory #xlsb

calamine

纯Rust编写的Excel/OpenDocument电子表格读取器和反序列化器

68个版本 (24个破坏性版本)

0.25.0 2024年5月24日
0.24.0 2024年2月8日
0.23.1 2023年12月19日
0.22.1 2023年10月8日
0.3.2 2016年11月27日

#33编码

Download history 15881/week @ 2024-05-03 20765/week @ 2024-05-10 18008/week @ 2024-05-17 16032/week @ 2024-05-24 17524/week @ 2024-05-31 15388/week @ 2024-06-07 16360/week @ 2024-06-14 18953/week @ 2024-06-21 16980/week @ 2024-06-28 19566/week @ 2024-07-05 22231/week @ 2024-07-12 21458/week @ 2024-07-19 20650/week @ 2024-07-26 22467/week @ 2024-08-02 24663/week @ 2024-08-09 21673/week @ 2024-08-16

每月 92,934次下载
用于 72 个crate (52 个直接使用)

MIT 许可

360KB
9K SLoC

calamine

纯Rust编写的Excel/OpenDocument电子表格文件读取器/反序列化器。

GitHub CI Rust tests Build status

文档

描述

calamine 是一个纯Rust库,用于读取和反序列化任何类似Excel的电子表格文件

  • (xls, xlsx, xlsm, xlsb, xla, xlam)
  • opendocument电子表格(ods)

只要你的文件足够简单,这个库就应该可以正常工作。对于其他任何事情,请提交一个包含失败测试的issue或发送一个pull request!

示例

Serde反序列化

就像这样简单

use calamine::{open_workbook, Error, Xlsx, Reader, RangeDeserializerBuilder};

fn example() -> Result<(), Error> {
    let path = format!("{}/tests/temperature.xlsx", env!("CARGO_MANIFEST_DIR"));
    let mut workbook: Xlsx<_> = open_workbook(path)?;
    let range = workbook.worksheet_range("Sheet1")
        .ok_or(Error::Msg("Cannot find 'Sheet1'"))??;

    let mut iter = RangeDeserializerBuilder::new().from_range(&range)?;

    if let Some(result) = iter.next() {
        let (label, value): (String, f64) = result?;
        assert_eq!(label, "celsius");
        assert_eq!(value, 22.2222);
        Ok(())
    } else {
        Err(From::from("expected at least one record but got none"))
    }
}

Calamine提供了处理无效类型值的辅助函数。例如,如果你想要反序列化一个应包含浮点数但可能也包含无效值(即字符串)的列,你可以使用Serde的deserialize_as_f64_or_none辅助函数与Serde的deserialize_with字段属性

use calamine::{deserialize_as_f64_or_none, open_workbook, RangeDeserializerBuilder, Reader, Xlsx};
use serde::Deserialize;

#[derive(Deserialize)]
struct Record {
    metric: String,
    #[serde(deserialize_with = "deserialize_as_f64_or_none")]
    value: Option<f64>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = format!("{}/tests/excel.xlsx", env!("CARGO_MANIFEST_DIR"));
    let mut excel: Xlsx<_> = open_workbook(path)?;

    let range = excel
        .worksheet_range("Sheet1")
        .map_err(|_| calamine::Error::Msg("Cannot find Sheet1"))?;

    let iter_records =
        RangeDeserializerBuilder::with_headers(&["metric", "value"]).from_range(&range)?;

    for result in iter_records {
        let record: Record = result?;
        println!("metric={:?}, value={:?}", record.metric, record.value);
    }

    Ok(())
}

deserialize_as_f64_or_none函数将丢弃所有无效值,如果你想将它们作为String返回,可以使用deserialize_as_f64_or_string函数代替。

读取器:简单

use calamine::{Reader, Xlsx, open_workbook};

let mut excel: Xlsx<_> = open_workbook("file.xlsx").unwrap();
if let Some(Ok(r)) = excel.worksheet_range("Sheet1") {
    for row in r.rows() {
        println!("row={:?}, row[0]={:?}", row, row[0]);
    }
}

读取器:更复杂

假设

  • 文件类型(xls, xlsx ...)在静态时间无法知道
  • 我们需要从工作簿中获取所有数据
  • 我们需要解析VBA
  • 我们需要查看定义的名称
  • 以及公式!
use calamine::{Reader, open_workbook_auto, Xlsx, DataType};

// opens a new workbook
let path = ...; // we do not know the file type
let mut workbook = open_workbook_auto(path).expect("Cannot open file");

// Read whole worksheet data and provide some statistics
if let Some(Ok(range)) = workbook.worksheet_range("Sheet1") {
    let total_cells = range.get_size().0 * range.get_size().1;
    let non_empty_cells: usize = range.used_cells().count();
    println!("Found {} cells in 'Sheet1', including {} non empty cells",
             total_cells, non_empty_cells);
    // alternatively, we can manually filter rows
    assert_eq!(non_empty_cells, range.rows()
        .flat_map(|r| r.iter().filter(|&c| c != &DataType::Empty)).count());
}

// Check if the workbook has a vba project
if let Some(Ok(mut vba)) = workbook.vba_project() {
    let vba = vba.to_mut();
    let module1 = vba.get_module("Module 1").unwrap();
    println!("Module 1 code:");
    println!("{}", module1);
    for r in vba.get_references() {
        if r.is_missing() {
            println!("Reference {} is broken or not accessible", r.name);
        }
    }
}

// You can also get defined names definition (string representation only)
for name in workbook.defined_names() {
    println!("name: {}, formula: {}", name.0, name.1);
}

// Now get all formula!
let sheets = workbook.sheet_names().to_owned();
for s in sheets {
    println!("found {} formula in '{}'",
             workbook
                .worksheet_formula(&s)
                .expect("sheet not found")
                .expect("error while getting formula")
                .rows().flat_map(|r| r.iter().filter(|f| !f.is_empty()))
                .count(),
             s);
}

功能

  • dates:向DataType添加与日期相关的函数。
  • picture:提取图片数据。

其他

浏览 示例 目录。

性能

由于 calamine 是只读的,比较将仅涉及读取 Excel xlsx 文件,然后遍历行。除了 calamine 之外,还选择了三个来自不同语言的库

基准测试使用的是这个 数据集,一个当 csv 转换时为 186MBxlsx 文件。绘图数据来自 sysinfo crate,采样间隔为 200ms。程序会采样运行进程报告的值并记录下来。

所有程序都遵循相同的结构

calamine:

use calamine::{open_workbook, Reader, Xlsx};

fn main() {
    // Open workbook 
    let mut excel: Xlsx<_> =
        open_workbook("NYC_311_SR_2010-2020-sample-1M.xlsx").expect("failed to find file");

    // Get worksheet
    let sheet = excel
        .worksheet_range("NYC_311_SR_2010-2020-sample-1M")
        .unwrap()
        .unwrap();

    // iterate over rows
    for _row in sheet.rows() {}
}

excelize:

package main

import (
        "fmt"
        "github.com/xuri/excelize/v2"
)

func main() {
        // Open workbook
        file, err := excelize.OpenFile(`NYC_311_SR_2010-2020-sample-1M.xlsx`)

        if err != nil {
                fmt.Println(err)
                return
        }

        defer func() {
                // Close the spreadsheet.
                if err := file.Close(); err != nil {
                        fmt.Println(err)
                }
        }()

        // Select worksheet
        rows, err := file.Rows("NYC_311_SR_2010-2020-sample-1M")
        if err != nil {
                fmt.Println(err)
                return
        }

        // Iterate over rows
        for rows.Next() {
        }
}

ClosedXML:

using ClosedXML.Excel;

internal class Program
{
        private static void Main(string[] args)
        {
                // Open workbook
                using var workbook = new XLWorkbook("NYC_311_SR_2010-2020-sample-1M.xlsx");

                // Get Worksheet
                // "NYC_311_SR_2010-2020-sample-1M"
                var worksheet = workbook.Worksheet(1);

                // Iterate over rows
                foreach (var row in worksheet.Rows())
                {

                }
        }
}

openpyxl:

from openpyxl import load_workbook

# Open workbook
wb = load_workbook(
    filename=r'NYC_311_SR_2010-2020-sample-1M.xlsx', read_only=True)

# Get worksheet
ws = wb['NYC_311_SR_2010-2020-sample-1M']

# Iterate over rows
for row in ws.rows:
    _ = row

# Close the workbook after reading
wb.close()

基准测试

基准测试使用 hyperfine 进行,使用 --warmup 3 在一个 AMD RYZEN 9 5900X @ 4.0GHz 上运行 Windows 11。两者 calamineClosedXML 都是在发布模式下构建的。

0.22.1 calamine.exe
  Time (mean ± σ):     25.278 s ±  0.424 s    [User: 24.852 s, System: 0.470 s]
  Range (min … max):   24.980 s … 26.369 s    10 runs

v2.8.0 excelize.exe
  Time (mean ± σ):     44.254 s ±  0.574 s    [User: 46.071 s, System: 7.754 s]
  Range (min … max):   42.947 s … 44.911 s    10 runs

0.102.1 closedxml.exe
  Time (mean ± σ):     178.343 s ±  3.673 s    [User: 177.442 s, System: 2.612 s]
  Range (min … max):   173.232 s … 185.086 s    10 runs

3.0.10 openpyxl.py
  Time (mean ± σ):     238.554 s ±  1.062 s    [User: 238.016 s, System: 0.661 s]
  Range (min … max):   236.798 s … 240.167 s    10 runs

calamine 比比 excelize 快 1.75 倍,比 ClosedXML 快 7.05 倍,比 openpyxl 快 9.43 倍。

电子表格的范围是 1,000,001 行和 41 列,总共有 41,000,041 个单元格在这个范围内。其中,28,056,975 个单元格有值。

根据这个数字

  • calamine => 每秒 1,122,279 个单元格
  • excelize => 每秒 633,998 个单元格
  • ClosedXML => 每秒 157,320 个单元格
  • openpyxl => 每秒 117,612 个单元格

图表

磁盘读取

bytes_from_disk

如上所述,磁盘上的文件大小是 186MB

  • calamine => 186MB
  • ClosedXML => 208MB
  • openpyxl => 192MB
  • excelize => 1.5GB

当我询问 excelize 的维护者时,我得到了这个 回复

为了避免读取大文件时内存使用量过高,此库允许用户在打开工作簿时指定 UnzipXMLSizeLimit 选项,以设置解压工作表和共享字符串表的字节内存限制,当文件大小超过此值时,工作表 XML 将被提取到系统临时目录,这样您可以看到以读取模式写入的数据,并且您可以更改此默认值以避免此行为。

- xuri

磁盘写入

bytes_to_disk

如前文所述,excelize 将数据写入磁盘以节省内存。其他库没有采用这种机制。

内存

mem_usage

virt_mem_usage

[!注意] ClosedXML 报告了恒定的 2.5TB 虚拟内存使用量,因此它被排除在图表之外。

calamine 的上升和下降来自于 Vec 的增长和紧接着的内存释放,内存使用量再次下降。图表末尾的突然上升是在将工作表读入内存时发生的。其他库由于垃圾收集,整个过程的增长更为线性。

CPU

cpu_usage

图表非常嘈杂,但 excelize 的峰值可能是由于 GC 引起的?

不支持

许多(大多数)规范尚未实现,重点放在读取单元格 VBA 代码上。

主要不支持的项目包括:

  • 不支持写入 Excel 文件,这是一个只读库
  • 不支持读取额外内容,如格式、Excel 参数、加密组件等 ...
  • 不支持读取用于 opendocuments 的 VB

致谢

感谢 xlsx-js 的开发者!这个库是目前我能找到的最简单的开源实现,并有助于理解官方文档。

还要感谢所有贡献者!

许可证

MIT

依赖

~8MB
~204K SLoC