Repository files navigation

calamine

An Excel/OpenDocument Spreadsheets file reader/deserializer, in pure Rust.

GitHub CI Rust testsBuild status

Documentation

Description

calamine is a pure Rust library to read and deserialize any spreadsheet file:

  • excel like (xls, xlsx, xlsm, xlsb, xla, xlam)
  • opendocument spreadsheets (ods)

As long as your files are simple enough, this library should just work. For anything else, please file an issue with a failing test or send a pull request!

Examples

Serde deserialization

It is as simple as:

use calamine::{open_workbook,Error,Xlsx,Reader,RangeDeserializerBuilder};fnexample() -> Result<(),Error>{let path = format!("{}/tests/temperature.xlsx", env!("CARGO_MANIFEST_DIR"));letmut workbook:Xlsx<_> = open_workbook(path)?;let range = workbook.worksheet_range("Sheet1").ok_or(Error::Msg("Cannot find 'Sheet1'"))??;letmut iter = RangeDeserializerBuilder::new().from_range(&range)?;ifletSome(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"))}}

Note if you want to deserialize a column that may have invalid types (i.e. a float where some values may be strings), you can use Serde's deserialize_with field attribute:

use serde::Deserialize;use calamine::{RangeDeserializerBuilder,Reader,Xlsx};#[derive(Deserialize)]structExcelRow{metric:String,#[serde(deserialize_with = "de_opt_f64")]value:Option<f64>,}// Convert value cell to Some(f64) if float or int, else Nonefnde_opt_f64<'de,D>(deserializer:D) -> Result<Option<f64>,D::Error>whereD: serde::Deserializer<'de>,{let data_type = calamine::DataType::deserialize(deserializer)?;ifletSome(float) = data_type.as_f64(){Ok(Some(float))}else{Ok(None)}}fnmain() -> Result<(),Box<dyn std::error::Error>>{let path = format!("{}/tests/excel.xlsx", env!("CARGO_MANIFEST_DIR"));letmut excel:Xlsx<_> = open_workbook(path)?;let range = excel
.worksheet_range("Sheet1").ok_or(calamine::Error::Msg("Cannot find Sheet1"))??;let iter_result =
RangeDeserializerBuilder::with_headers(&COLUMNS).from_range::<_,ExcelRow>(&range)?;}

Reader: Simple

use calamine::{Reader,Xlsx, open_workbook};letmut excel:Xlsx<_> = open_workbook("file.xlsx").unwrap();ifletSome(Ok(r)) = excel.worksheet_range("Sheet1"){for row in r.rows(){println!("row={:?}, row[0]={:?}", row, row[0]);}}

Reader: More complex

Let's assume

  • the file type (xls, xlsx ...) cannot be known at static time
  • we need to get all data from the workbook
  • we need to parse the vba
  • we need to see the defined names
  • and the formula!
use calamine::{Reader, open_workbook_auto,Xlsx,DataType};// opens a new workbooklet path = ...;// we do not know the file typeletmut workbook = open_workbook_auto(path).expect("Cannot open file");// Read whole worksheet data and provide some statisticsifletSome(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 rowsassert_eq!(non_empty_cells, range.rows().flat_map(|r| r.iter().filter(|&c| c != &DataType::Empty)).count());}// Check if the workbook has a vba projectifletSome(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);}

Features

  • dates: Add date related fn to DataType.
  • picture: Extract picture data.

Others

Browse the examples directory.

Performance

As calamine is readonly, the comparisons will only involve reading an excel xlsx file and then iterating over the rows. Along with calamine, three other libraries were chosen, from three different languages:

The benchmarks were done using this dataset, a 186MBxlsx file when the csv is converted. The plotting data was gotten from the sysinfo crate, at a sample interval of 200ms. The program samples the reported values for the running process and records it.

The programs are all structured to follow the same constructs:

calamine:

use calamine::{open_workbook,Reader,Xlsx};fnmain(){// Open workbook letmut excel:Xlsx<_> =
open_workbook("NYC_311_SR_2010-2020-sample-1M.xlsx").expect("failed to find file");// Get worksheetlet sheet = excel
.worksheet_range("NYC_311_SR_2010-2020-sample-1M").unwrap().unwrap();// iterate over rowsfor _row in sheet.rows(){}}

excelize:

package main
import (
"fmt""github.com/xuri/excelize/v2"
)
funcmain() {
// Open workbookfile, err:=excelize.OpenFile(`NYC_311_SR_2010-2020-sample-1M.xlsx`)
iferr!=nil {
fmt.Println(err)
return
}
deferfunc() {
// Close the spreadsheet.iferr:=file.Close(); err!=nil {
fmt.Println(err)
}
}()
// Select worksheetrows, err:=file.Rows("NYC_311_SR_2010-2020-sample-1M")
iferr!=nil {
fmt.Println(err)
return
}
// Iterate over rowsforrows.Next() {
}
}

ClosedXML:

usingClosedXML.Excel;internalclassProgram{privatestaticvoidMain(string[]args){// Open workbookusingvarworkbook=newXLWorkbook("NYC_311_SR_2010-2020-sample-1M.xlsx");// Get Worksheet// "NYC_311_SR_2010-2020-sample-1M"varworksheet=workbook.Worksheet(1);// Iterate over rowsforeach(varrowinworksheet.Rows()){}}}

openpyxl:

fromopenpyxlimportload_workbook# Open workbookwb=load_workbook(
filename=r'NYC_311_SR_2010-2020-sample-1M.xlsx', read_only=True)
# Get worksheetws=wb['NYC_311_SR_2010-2020-sample-1M']
# Iterate over rowsforrowinws.rows:
_=row# Close the workbook after readingwb.close()

Benchmarks

The benchmarking was done using hyperfine with --warmup 3 on an AMD RYZEN 9 5900X @ 4.0GHz running Windows 11. Both calamine and ClosedXML were built in release mode.

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 is 1.75x faster than excelize, 7.05x faster than ClosedXML, and 9.43x faster than openpyxl.

The spreadsheet has a range of 1,000,001 rows and 41 columns, for a total of 41,000,041 cells in the range. Of those, 28,056,975 cells had values.

Going off of that number:

  • calamine => 1,122,279 cells per second
  • excelize => 633,998 cells per second
  • ClosedXML => 157,320 cells per second
  • openpyxl => 117,612 cells per second

Plots

Disk Read

bytes_from_disk

As stated, the filesize on disk is 186MB:

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

When asking one of the maintainers of excelize, I got this response:

To avoid high memory usage for reading large files, this library allows user-specific UnzipXMLSizeLimit options when opening the workbook, to set the memory limit on the unzipping worksheet and shared string table in bytes, worksheet XML will be extracted to the system temporary directory when the file size is over this value, so you can see that data written in reading mode, and you can change the default for that to avoid this behavior.

- xuri

Disk Write

bytes_to_disk

As seen in the previous section, excelize is writting to disk to save memory. The others don't employ that kind of mechanism.

Memory

mem_usage

virt_mem_usage

Note

ClosedXML was reporting a constant 2.5TB of virtual memory usage, so it was excluded from the chart.

The stepping and falling for calamine is from the grows of Vecs and the freeing of memory right after, with the memory usage dropping down again. The sudden jump at the end is when the sheet is being read into memory. The others, being garbage collected, have a more linear climb all the way through.

CPU

cpu_usage

Very noisy chart, but excelize's spikes must be from the GC?

Unsupported

Many (most) part of the specifications are not implemented, the focus has been put on reading cell values and vba code.

The main unsupported items are:

  • no support for writing excel files, this is a read-only library
  • no support for reading extra contents, such as formatting, excel parameter, encrypted components etc ...
  • no support for reading VB for opendocuments

Credits

Thanks to xlsx-js developers! This library is by far the simplest open source implementation I could find and helps making sense out of official documentation.

Thanks also to all the contributors!

License

MIT

About

A pure Rust Excel/OpenDocument SpreadSheets file reader: rust on metal sheets

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

calamine

An Excel/OpenDocument Spreadsheets file reader/deserializer, in pure Rust.

GitHub CI Rust testsBuild status

Documentation

Description

calamine is a pure Rust library to read and deserialize any spreadsheet file:

  • excel like (xls, xlsx, xlsm, xlsb, xla, xlam)
  • opendocument spreadsheets (ods)

As long as your files are simple enough, this library should just work. For anything else, please file an issue with a failing test or send a pull request!

Examples

Serde deserialization

It is as simple as:

use calamine::{open_workbook,Error,Xlsx,Reader,RangeDeserializerBuilder};fnexample() -> Result<(),Error>{let path = format!("{}/tests/temperature.xlsx", env!("CARGO_MANIFEST_DIR"));letmut workbook:Xlsx<_> = open_workbook(path)?;let range = workbook.worksheet_range("Sheet1").ok_or(Error::Msg("Cannot find 'Sheet1'"))??;letmut iter = RangeDeserializerBuilder::new().from_range(&range)?;ifletSome(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"))}}

Note if you want to deserialize a column that may have invalid types (i.e. a float where some values may be strings), you can use Serde's deserialize_with field attribute:

use serde::Deserialize;use calamine::{RangeDeserializerBuilder,Reader,Xlsx};#[derive(Deserialize)]structExcelRow{metric:String,#[serde(deserialize_with = "de_opt_f64")]value:Option<f64>,}// Convert value cell to Some(f64) if float or int, else Nonefnde_opt_f64<'de,D>(deserializer:D) -> Result<Option<f64>,D::Error>whereD: serde::Deserializer<'de>,{let data_type = calamine::DataType::deserialize(deserializer)?;ifletSome(float) = data_type.as_f64(){Ok(Some(float))}else{Ok(None)}}fnmain() -> Result<(),Box<dyn std::error::Error>>{let path = format!("{}/tests/excel.xlsx", env!("CARGO_MANIFEST_DIR"));letmut excel:Xlsx<_> = open_workbook(path)?;let range = excel
.worksheet_range("Sheet1").ok_or(calamine::Error::Msg("Cannot find Sheet1"))??;let iter_result =
RangeDeserializerBuilder::with_headers(&COLUMNS).from_range::<_,ExcelRow>(&range)?;}

Reader: Simple

use calamine::{Reader,Xlsx, open_workbook};letmut excel:Xlsx<_> = open_workbook("file.xlsx").unwrap();ifletSome(Ok(r)) = excel.worksheet_range("Sheet1"){for row in r.rows(){println!("row={:?}, row[0]={:?}", row, row[0]);}}

Reader: More complex

Let's assume

  • the file type (xls, xlsx ...) cannot be known at static time
  • we need to get all data from the workbook
  • we need to parse the vba
  • we need to see the defined names
  • and the formula!
use calamine::{Reader, open_workbook_auto,Xlsx,DataType};// opens a new workbooklet path = ...;// we do not know the file typeletmut workbook = open_workbook_auto(path).expect("Cannot open file");// Read whole worksheet data and provide some statisticsifletSome(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 rowsassert_eq!(non_empty_cells, range.rows().flat_map(|r| r.iter().filter(|&c| c != &DataType::Empty)).count());}// Check if the workbook has a vba projectifletSome(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);}

Features

  • dates: Add date related fn to DataType.
  • picture: Extract picture data.

Others

Browse the examples directory.

Performance

As calamine is readonly, the comparisons will only involve reading an excel xlsx file and then iterating over the rows. Along with calamine, three other libraries were chosen, from three different languages:

The benchmarks were done using this dataset, a 186MBxlsx file when the csv is converted. The plotting data was gotten from the sysinfo crate, at a sample interval of 200ms. The program samples the reported values for the running process and records it.

The programs are all structured to follow the same constructs:

calamine:

use calamine::{open_workbook,Reader,Xlsx};fnmain(){// Open workbook letmut excel:Xlsx<_> =
open_workbook("NYC_311_SR_2010-2020-sample-1M.xlsx").expect("failed to find file");// Get worksheetlet sheet = excel
.worksheet_range("NYC_311_SR_2010-2020-sample-1M").unwrap().unwrap();// iterate over rowsfor _row in sheet.rows(){}}

excelize:

package main
import (
"fmt""github.com/xuri/excelize/v2"
)
funcmain() {
// Open workbookfile, err:=excelize.OpenFile(`NYC_311_SR_2010-2020-sample-1M.xlsx`)
iferr!=nil {
fmt.Println(err)
return
}
deferfunc() {
// Close the spreadsheet.iferr:=file.Close(); err!=nil {
fmt.Println(err)
}
}()
// Select worksheetrows, err:=file.Rows("NYC_311_SR_2010-2020-sample-1M")
iferr!=nil {
fmt.Println(err)
return
}
// Iterate over rowsforrows.Next() {
}
}

ClosedXML:

usingClosedXML.Excel;internalclassProgram{privatestaticvoidMain(string[]args){// Open workbookusingvarworkbook=newXLWorkbook("NYC_311_SR_2010-2020-sample-1M.xlsx");// Get Worksheet// "NYC_311_SR_2010-2020-sample-1M"varworksheet=workbook.Worksheet(1);// Iterate over rowsforeach(varrowinworksheet.Rows()){}}}

openpyxl:

fromopenpyxlimportload_workbook# Open workbookwb=load_workbook(
filename=r'NYC_311_SR_2010-2020-sample-1M.xlsx', read_only=True)
# Get worksheetws=wb['NYC_311_SR_2010-2020-sample-1M']
# Iterate over rowsforrowinws.rows:
_=row# Close the workbook after readingwb.close()

Benchmarks

The benchmarking was done using hyperfine with --warmup 3 on an AMD RYZEN 9 5900X @ 4.0GHz running Windows 11. Both calamine and ClosedXML were built in release mode.

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 is 1.75x faster than excelize, 7.05x faster than ClosedXML, and 9.43x faster than openpyxl.

The spreadsheet has a range of 1,000,001 rows and 41 columns, for a total of 41,000,041 cells in the range. Of those, 28,056,975 cells had values.

Going off of that number:

  • calamine => 1,122,279 cells per second
  • excelize => 633,998 cells per second
  • ClosedXML => 157,320 cells per second
  • openpyxl => 117,612 cells per second

Plots

Disk Read

bytes_from_disk

As stated, the filesize on disk is 186MB:

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

When asking one of the maintainers of excelize, I got this response:

To avoid high memory usage for reading large files, this library allows user-specific UnzipXMLSizeLimit options when opening the workbook, to set the memory limit on the unzipping worksheet and shared string table in bytes, worksheet XML will be extracted to the system temporary directory when the file size is over this value, so you can see that data written in reading mode, and you can change the default for that to avoid this behavior.

- xuri

Disk Write

bytes_to_disk

As seen in the previous section, excelize is writting to disk to save memory. The others don't employ that kind of mechanism.

Memory

mem_usage

virt_mem_usage

Note

ClosedXML was reporting a constant 2.5TB of virtual memory usage, so it was excluded from the chart.

The stepping and falling for calamine is from the grows of Vecs and the freeing of memory right after, with the memory usage dropping down again. The sudden jump at the end is when the sheet is being read into memory. The others, being garbage collected, have a more linear climb all the way through.

CPU

cpu_usage

Very noisy chart, but excelize's spikes must be from the GC?

Unsupported

Many (most) part of the specifications are not implemented, the focus has been put on reading cell values and vba code.

The main unsupported items are:

  • no support for writing excel files, this is a read-only library
  • no support for reading extra contents, such as formatting, excel parameter, encrypted components etc ...
  • no support for reading VB for opendocuments

Credits

Thanks to xlsx-js developers! This library is by far the simplest open source implementation I could find and helps making sense out of official documentation.

Thanks also to all the contributors!

License

MIT

About

A pure Rust Excel/OpenDocument SpreadSheets file reader: rust on metal sheets

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

calamine

An Excel/OpenDocument Spreadsheets file reader/deserializer, in pure Rust.

GitHub CI Rust testsBuild status

Documentation

Description

calamine is a pure Rust library to read and deserialize any spreadsheet file:

  • excel like (xls, xlsx, xlsm, xlsb, xla, xlam)
  • opendocument spreadsheets (ods)

As long as your files are simple enough, this library should just work. For anything else, please file an issue with a failing test or send a pull request!

Examples

Serde deserialization

It is as simple as:

use calamine::{open_workbook,Error,Xlsx,Reader,RangeDeserializerBuilder};fnexample() -> Result<(),Error>{let path = format!("{}/tests/temperature.xlsx", env!("CARGO_MANIFEST_DIR"));letmut workbook:Xlsx<_> = open_workbook(path)?;let range = workbook.worksheet_range("Sheet1").ok_or(Error::Msg("Cannot find 'Sheet1'"))??;letmut iter = RangeDeserializerBuilder::new().from_range(&range)?;ifletSome(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"))}}

Note if you want to deserialize a column that may have invalid types (i.e. a float where some values may be strings), you can use Serde's deserialize_with field attribute:

use serde::Deserialize;use calamine::{RangeDeserializerBuilder,Reader,Xlsx};#[derive(Deserialize)]structExcelRow{metric:String,#[serde(deserialize_with = "de_opt_f64")]value:Option<f64>,}// Convert value cell to Some(f64) if float or int, else Nonefnde_opt_f64<'de,D>(deserializer:D) -> Result<Option<f64>,D::Error>whereD: serde::Deserializer<'de>,{let data_type = calamine::DataType::deserialize(deserializer)?;ifletSome(float) = data_type.as_f64(){Ok(Some(float))}else{Ok(None)}}fnmain() -> Result<(),Box<dyn std::error::Error>>{let path = format!("{}/tests/excel.xlsx", env!("CARGO_MANIFEST_DIR"));letmut excel:Xlsx<_> = open_workbook(path)?;let range = excel
.worksheet_range("Sheet1").ok_or(calamine::Error::Msg("Cannot find Sheet1"))??;let iter_result =
RangeDeserializerBuilder::with_headers(&COLUMNS).from_range::<_,ExcelRow>(&range)?;}

Reader: Simple

use calamine::{Reader,Xlsx, open_workbook};letmut excel:Xlsx<_> = open_workbook("file.xlsx").unwrap();ifletSome(Ok(r)) = excel.worksheet_range("Sheet1"){for row in r.rows(){println!("row={:?}, row[0]={:?}", row, row[0]);}}

Reader: More complex

Let's assume

  • the file type (xls, xlsx ...) cannot be known at static time
  • we need to get all data from the workbook
  • we need to parse the vba
  • we need to see the defined names
  • and the formula!
use calamine::{Reader, open_workbook_auto,Xlsx,DataType};// opens a new workbooklet path = ...;// we do not know the file typeletmut workbook = open_workbook_auto(path).expect("Cannot open file");// Read whole worksheet data and provide some statisticsifletSome(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 rowsassert_eq!(non_empty_cells, range.rows().flat_map(|r| r.iter().filter(|&c| c != &DataType::Empty)).count());}// Check if the workbook has a vba projectifletSome(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);}

Features

  • dates: Add date related fn to DataType.
  • picture: Extract picture data.

Others

Browse the examples directory.

Performance

As calamine is readonly, the comparisons will only involve reading an excel xlsx file and then iterating over the rows. Along with calamine, three other libraries were chosen, from three different languages:

The benchmarks were done using this dataset, a 186MBxlsx file when the csv is converted. The plotting data was gotten from the sysinfo crate, at a sample interval of 200ms. The program samples the reported values for the running process and records it.

The programs are all structured to follow the same constructs:

calamine:

use calamine::{open_workbook,Reader,Xlsx};fnmain(){// Open workbook letmut excel:Xlsx<_> =
open_workbook("NYC_311_SR_2010-2020-sample-1M.xlsx").expect("failed to find file");// Get worksheetlet sheet = excel
.worksheet_range("NYC_311_SR_2010-2020-sample-1M").unwrap().unwrap();// iterate over rowsfor _row in sheet.rows(){}}

excelize:

package main
import (
"fmt""github.com/xuri/excelize/v2"
)
funcmain() {
// Open workbookfile, err:=excelize.OpenFile(`NYC_311_SR_2010-2020-sample-1M.xlsx`)
iferr!=nil {
fmt.Println(err)
return
}
deferfunc() {
// Close the spreadsheet.iferr:=file.Close(); err!=nil {
fmt.Println(err)
}
}()
// Select worksheetrows, err:=file.Rows("NYC_311_SR_2010-2020-sample-1M")
iferr!=nil {
fmt.Println(err)
return
}
// Iterate over rowsforrows.Next() {
}
}

ClosedXML:

usingClosedXML.Excel;internalclassProgram{privatestaticvoidMain(string[]args){// Open workbookusingvarworkbook=newXLWorkbook("NYC_311_SR_2010-2020-sample-1M.xlsx");// Get Worksheet// "NYC_311_SR_2010-2020-sample-1M"varworksheet=workbook.Worksheet(1);// Iterate over rowsforeach(varrowinworksheet.Rows()){}}}

openpyxl:

fromopenpyxlimportload_workbook# Open workbookwb=load_workbook(
filename=r'NYC_311_SR_2010-2020-sample-1M.xlsx', read_only=True)
# Get worksheetws=wb['NYC_311_SR_2010-2020-sample-1M']
# Iterate over rowsforrowinws.rows:
_=row# Close the workbook after readingwb.close()

Benchmarks

The benchmarking was done using hyperfine with --warmup 3 on an AMD RYZEN 9 5900X @ 4.0GHz running Windows 11. Both calamine and ClosedXML were built in release mode.

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 is 1.75x faster than excelize, 7.05x faster than ClosedXML, and 9.43x faster than openpyxl.

The spreadsheet has a range of 1,000,001 rows and 41 columns, for a total of 41,000,041 cells in the range. Of those, 28,056,975 cells had values.

Going off of that number:

  • calamine => 1,122,279 cells per second
  • excelize => 633,998 cells per second
  • ClosedXML => 157,320 cells per second
  • openpyxl => 117,612 cells per second

Plots

Disk Read

bytes_from_disk

As stated, the filesize on disk is 186MB:

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

When asking one of the maintainers of excelize, I got this response:

To avoid high memory usage for reading large files, this library allows user-specific UnzipXMLSizeLimit options when opening the workbook, to set the memory limit on the unzipping worksheet and shared string table in bytes, worksheet XML will be extracted to the system temporary directory when the file size is over this value, so you can see that data written in reading mode, and you can change the default for that to avoid this behavior.

- xuri

Disk Write

bytes_to_disk

As seen in the previous section, excelize is writting to disk to save memory. The others don't employ that kind of mechanism.

Memory

mem_usage

virt_mem_usage

Note

ClosedXML was reporting a constant 2.5TB of virtual memory usage, so it was excluded from the chart.

The stepping and falling for calamine is from the grows of Vecs and the freeing of memory right after, with the memory usage dropping down again. The sudden jump at the end is when the sheet is being read into memory. The others, being garbage collected, have a more linear climb all the way through.

CPU

cpu_usage

Very noisy chart, but excelize's spikes must be from the GC?

Unsupported

Many (most) part of the specifications are not implemented, the focus has been put on reading cell values and vba code.

The main unsupported items are:

  • no support for writing excel files, this is a read-only library
  • no support for reading extra contents, such as formatting, excel parameter, encrypted components etc ...
  • no support for reading VB for opendocuments

Credits

Thanks to xlsx-js developers! This library is by far the simplest open source implementation I could find and helps making sense out of official documentation.

Thanks also to all the contributors!

License

MIT

About

A pure Rust Excel/OpenDocument SpreadSheets file reader: rust on metal sheets

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

calamine

An Excel/OpenDocument Spreadsheets file reader/deserializer, in pure Rust.

GitHub CI Rust testsBuild status

Documentation

Description

calamine is a pure Rust library to read and deserialize any spreadsheet file:

  • excel like (xls, xlsx, xlsm, xlsb, xla, xlam)
  • opendocument spreadsheets (ods)

As long as your files are simple enough, this library should just work. For anything else, please file an issue with a failing test or send a pull request!

Examples

Serde deserialization

It is as simple as:

use calamine::{open_workbook,Error,Xlsx,Reader,RangeDeserializerBuilder};fnexample() -> Result<(),Error>{let path = format!("{}/tests/temperature.xlsx", env!("CARGO_MANIFEST_DIR"));letmut workbook:Xlsx<_> = open_workbook(path)?;let range = workbook.worksheet_range("Sheet1").ok_or(Error::Msg("Cannot find 'Sheet1'"))??;letmut iter = RangeDeserializerBuilder::new().from_range(&range)?;ifletSome(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"))}}

Note if you want to deserialize a column that may have invalid types (i.e. a float where some values may be strings), you can use Serde's deserialize_with field attribute:

use serde::Deserialize;use calamine::{RangeDeserializerBuilder,Reader,Xlsx};#[derive(Deserialize)]structExcelRow{metric:String,#[serde(deserialize_with = "de_opt_f64")]value:Option<f64>,}// Convert value cell to Some(f64) if float or int, else Nonefnde_opt_f64<'de,D>(deserializer:D) -> Result<Option<f64>,D::Error>whereD: serde::Deserializer<'de>,{let data_type = calamine::DataType::deserialize(deserializer)?;ifletSome(float) = data_type.as_f64(){Ok(Some(float))}else{Ok(None)}}fnmain() -> Result<(),Box<dyn std::error::Error>>{let path = format!("{}/tests/excel.xlsx", env!("CARGO_MANIFEST_DIR"));letmut excel:Xlsx<_> = open_workbook(path)?;let range = excel
.worksheet_range("Sheet1").ok_or(calamine::Error::Msg("Cannot find Sheet1"))??;let iter_result =
RangeDeserializerBuilder::with_headers(&COLUMNS).from_range::<_,ExcelRow>(&range)?;}

Reader: Simple

use calamine::{Reader,Xlsx, open_workbook};letmut excel:Xlsx<_> = open_workbook("file.xlsx").unwrap();ifletSome(Ok(r)) = excel.worksheet_range("Sheet1"){for row in r.rows(){println!("row={:?}, row[0]={:?}", row, row[0]);}}

Reader: More complex

Let's assume

  • the file type (xls, xlsx ...) cannot be known at static time
  • we need to get all data from the workbook
  • we need to parse the vba
  • we need to see the defined names
  • and the formula!
use calamine::{Reader, open_workbook_auto,Xlsx,DataType};// opens a new workbooklet path = ...;// we do not know the file typeletmut workbook = open_workbook_auto(path).expect("Cannot open file");// Read whole worksheet data and provide some statisticsifletSome(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 rowsassert_eq!(non_empty_cells, range.rows().flat_map(|r| r.iter().filter(|&c| c != &DataType::Empty)).count());}// Check if the workbook has a vba projectifletSome(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);}

Features

  • dates: Add date related fn to DataType.
  • picture: Extract picture data.

Others

Browse the examples directory.

Performance

As calamine is readonly, the comparisons will only involve reading an excel xlsx file and then iterating over the rows. Along with calamine, three other libraries were chosen, from three different languages:

The benchmarks were done using this dataset, a 186MBxlsx file when the csv is converted. The plotting data was gotten from the sysinfo crate, at a sample interval of 200ms. The program samples the reported values for the running process and records it.

The programs are all structured to follow the same constructs:

calamine:

use calamine::{open_workbook,Reader,Xlsx};fnmain(){// Open workbook letmut excel:Xlsx<_> =
open_workbook("NYC_311_SR_2010-2020-sample-1M.xlsx").expect("failed to find file");// Get worksheetlet sheet = excel
.worksheet_range("NYC_311_SR_2010-2020-sample-1M").unwrap().unwrap();// iterate over rowsfor _row in sheet.rows(){}}

excelize:

package main
import (
"fmt""github.com/xuri/excelize/v2"
)
funcmain() {
// Open workbookfile, err:=excelize.OpenFile(`NYC_311_SR_2010-2020-sample-1M.xlsx`)
iferr!=nil {
fmt.Println(err)
return
}
deferfunc() {
// Close the spreadsheet.iferr:=file.Close(); err!=nil {
fmt.Println(err)
}
}()
// Select worksheetrows, err:=file.Rows("NYC_311_SR_2010-2020-sample-1M")
iferr!=nil {
fmt.Println(err)
return
}
// Iterate over rowsforrows.Next() {
}
}

ClosedXML:

usingClosedXML.Excel;internalclassProgram{privatestaticvoidMain(string[]args){// Open workbookusingvarworkbook=newXLWorkbook("NYC_311_SR_2010-2020-sample-1M.xlsx");// Get Worksheet// "NYC_311_SR_2010-2020-sample-1M"varworksheet=workbook.Worksheet(1);// Iterate over rowsforeach(varrowinworksheet.Rows()){}}}

openpyxl:

fromopenpyxlimportload_workbook# Open workbookwb=load_workbook(
filename=r'NYC_311_SR_2010-2020-sample-1M.xlsx', read_only=True)
# Get worksheetws=wb['NYC_311_SR_2010-2020-sample-1M']
# Iterate over rowsforrowinws.rows:
_=row# Close the workbook after readingwb.close()

Benchmarks

The benchmarking was done using hyperfine with --warmup 3 on an AMD RYZEN 9 5900X @ 4.0GHz running Windows 11. Both calamine and ClosedXML were built in release mode.

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 is 1.75x faster than excelize, 7.05x faster than ClosedXML, and 9.43x faster than openpyxl.

The spreadsheet has a range of 1,000,001 rows and 41 columns, for a total of 41,000,041 cells in the range. Of those, 28,056,975 cells had values.

Going off of that number:

  • calamine => 1,122,279 cells per second
  • excelize => 633,998 cells per second
  • ClosedXML => 157,320 cells per second
  • openpyxl => 117,612 cells per second

Plots

Disk Read

bytes_from_disk

As stated, the filesize on disk is 186MB:

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

When asking one of the maintainers of excelize, I got this response:

To avoid high memory usage for reading large files, this library allows user-specific UnzipXMLSizeLimit options when opening the workbook, to set the memory limit on the unzipping worksheet and shared string table in bytes, worksheet XML will be extracted to the system temporary directory when the file size is over this value, so you can see that data written in reading mode, and you can change the default for that to avoid this behavior.

- xuri

Disk Write

bytes_to_disk

As seen in the previous section, excelize is writting to disk to save memory. The others don't employ that kind of mechanism.

Memory

mem_usage

virt_mem_usage

Note

ClosedXML was reporting a constant 2.5TB of virtual memory usage, so it was excluded from the chart.

The stepping and falling for calamine is from the grows of Vecs and the freeing of memory right after, with the memory usage dropping down again. The sudden jump at the end is when the sheet is being read into memory. The others, being garbage collected, have a more linear climb all the way through.

CPU

cpu_usage

Very noisy chart, but excelize's spikes must be from the GC?

Unsupported

Many (most) part of the specifications are not implemented, the focus has been put on reading cell values and vba code.

The main unsupported items are:

  • no support for writing excel files, this is a read-only library
  • no support for reading extra contents, such as formatting, excel parameter, encrypted components etc ...
  • no support for reading VB for opendocuments

Credits

Thanks to xlsx-js developers! This library is by far the simplest open source implementation I could find and helps making sense out of official documentation.

Thanks also to all the contributors!

License

MIT

About

A pure Rust Excel/OpenDocument SpreadSheets file reader: rust on metal sheets

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

calamine

An Excel/OpenDocument Spreadsheets file reader/deserializer, in pure Rust.

GitHub CI Rust testsBuild status

Documentation

Description

calamine is a pure Rust library to read and deserialize any spreadsheet file:

  • excel like (xls, xlsx, xlsm, xlsb, xla, xlam)
  • opendocument spreadsheets (ods)

As long as your files are simple enough, this library should just work. For anything else, please file an issue with a failing test or send a pull request!

Examples

Serde deserialization

It is as simple as:

use calamine::{open_workbook,Error,Xlsx,Reader,RangeDeserializerBuilder};fnexample() -> Result<(),Error>{let path = format!("{}/tests/temperature.xlsx", env!("CARGO_MANIFEST_DIR"));letmut workbook:Xlsx<_> = open_workbook(path)?;let range = workbook.worksheet_range("Sheet1").ok_or(Error::Msg("Cannot find 'Sheet1'"))??;letmut iter = RangeDeserializerBuilder::new().from_range(&range)?;ifletSome(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"))}}

Note if you want to deserialize a column that may have invalid types (i.e. a float where some values may be strings), you can use Serde's deserialize_with field attribute:

use serde::Deserialize;use calamine::{RangeDeserializerBuilder,Reader,Xlsx};#[derive(Deserialize)]structExcelRow{metric:String,#[serde(deserialize_with = "de_opt_f64")]value:Option<f64>,}// Convert value cell to Some(f64) if float or int, else Nonefnde_opt_f64<'de,D>(deserializer:D) -> Result<Option<f64>,D::Error>whereD: serde::Deserializer<'de>,{let data_type = calamine::DataType::deserialize(deserializer)?;ifletSome(float) = data_type.as_f64(){Ok(Some(float))}else{Ok(None)}}fnmain() -> Result<(),Box<dyn std::error::Error>>{let path = format!("{}/tests/excel.xlsx", env!("CARGO_MANIFEST_DIR"));letmut excel:Xlsx<_> = open_workbook(path)?;let range = excel
.worksheet_range("Sheet1").ok_or(calamine::Error::Msg("Cannot find Sheet1"))??;let iter_result =
RangeDeserializerBuilder::with_headers(&COLUMNS).from_range::<_,ExcelRow>(&range)?;}

Reader: Simple

use calamine::{Reader,Xlsx, open_workbook};letmut excel:Xlsx<_> = open_workbook("file.xlsx").unwrap();ifletSome(Ok(r)) = excel.worksheet_range("Sheet1"){for row in r.rows(){println!("row={:?}, row[0]={:?}", row, row[0]);}}

Reader: More complex

Let's assume

  • the file type (xls, xlsx ...) cannot be known at static time
  • we need to get all data from the workbook
  • we need to parse the vba
  • we need to see the defined names
  • and the formula!
use calamine::{Reader, open_workbook_auto,Xlsx,DataType};// opens a new workbooklet path = ...;// we do not know the file typeletmut workbook = open_workbook_auto(path).expect("Cannot open file");// Read whole worksheet data and provide some statisticsifletSome(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 rowsassert_eq!(non_empty_cells, range.rows().flat_map(|r| r.iter().filter(|&c| c != &DataType::Empty)).count());}// Check if the workbook has a vba projectifletSome(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);}

Features

  • dates: Add date related fn to DataType.
  • picture: Extract picture data.

Others

Browse the examples directory.

Performance

As calamine is readonly, the comparisons will only involve reading an excel xlsx file and then iterating over the rows. Along with calamine, three other libraries were chosen, from three different languages:

The benchmarks were done using this dataset, a 186MBxlsx file when the csv is converted. The plotting data was gotten from the sysinfo crate, at a sample interval of 200ms. The program samples the reported values for the running process and records it.

The programs are all structured to follow the same constructs:

calamine:

use calamine::{open_workbook,Reader,Xlsx};fnmain(){// Open workbook letmut excel:Xlsx<_> =
open_workbook("NYC_311_SR_2010-2020-sample-1M.xlsx").expect("failed to find file");// Get worksheetlet sheet = excel
.worksheet_range("NYC_311_SR_2010-2020-sample-1M").unwrap().unwrap();// iterate over rowsfor _row in sheet.rows(){}}

excelize:

package main
import (
"fmt""github.com/xuri/excelize/v2"
)
funcmain() {
// Open workbookfile, err:=excelize.OpenFile(`NYC_311_SR_2010-2020-sample-1M.xlsx`)
iferr!=nil {
fmt.Println(err)
return
}
deferfunc() {
// Close the spreadsheet.iferr:=file.Close(); err!=nil {
fmt.Println(err)
}
}()
// Select worksheetrows, err:=file.Rows("NYC_311_SR_2010-2020-sample-1M")
iferr!=nil {
fmt.Println(err)
return
}
// Iterate over rowsforrows.Next() {
}
}

ClosedXML:

usingClosedXML.Excel;internalclassProgram{privatestaticvoidMain(string[]args){// Open workbookusingvarworkbook=newXLWorkbook("NYC_311_SR_2010-2020-sample-1M.xlsx");// Get Worksheet// "NYC_311_SR_2010-2020-sample-1M"varworksheet=workbook.Worksheet(1);// Iterate over rowsforeach(varrowinworksheet.Rows()){}}}

openpyxl:

fromopenpyxlimportload_workbook# Open workbookwb=load_workbook(
filename=r'NYC_311_SR_2010-2020-sample-1M.xlsx', read_only=True)
# Get worksheetws=wb['NYC_311_SR_2010-2020-sample-1M']
# Iterate over rowsforrowinws.rows:
_=row# Close the workbook after readingwb.close()

Benchmarks

The benchmarking was done using hyperfine with --warmup 3 on an AMD RYZEN 9 5900X @ 4.0GHz running Windows 11. Both calamine and ClosedXML were built in release mode.

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 is 1.75x faster than excelize, 7.05x faster than ClosedXML, and 9.43x faster than openpyxl.

The spreadsheet has a range of 1,000,001 rows and 41 columns, for a total of 41,000,041 cells in the range. Of those, 28,056,975 cells had values.

Going off of that number:

  • calamine => 1,122,279 cells per second
  • excelize => 633,998 cells per second
  • ClosedXML => 157,320 cells per second
  • openpyxl => 117,612 cells per second

Plots

Disk Read

bytes_from_disk

As stated, the filesize on disk is 186MB:

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

When asking one of the maintainers of excelize, I got this response:

To avoid high memory usage for reading large files, this library allows user-specific UnzipXMLSizeLimit options when opening the workbook, to set the memory limit on the unzipping worksheet and shared string table in bytes, worksheet XML will be extracted to the system temporary directory when the file size is over this value, so you can see that data written in reading mode, and you can change the default for that to avoid this behavior.

- xuri

Disk Write

bytes_to_disk

As seen in the previous section, excelize is writting to disk to save memory. The others don't employ that kind of mechanism.

Memory

mem_usage

virt_mem_usage

Note

ClosedXML was reporting a constant 2.5TB of virtual memory usage, so it was excluded from the chart.

The stepping and falling for calamine is from the grows of Vecs and the freeing of memory right after, with the memory usage dropping down again. The sudden jump at the end is when the sheet is being read into memory. The others, being garbage collected, have a more linear climb all the way through.

CPU

cpu_usage

Very noisy chart, but excelize's spikes must be from the GC?

Unsupported

Many (most) part of the specifications are not implemented, the focus has been put on reading cell values and vba code.

The main unsupported items are:

  • no support for writing excel files, this is a read-only library
  • no support for reading extra contents, such as formatting, excel parameter, encrypted components etc ...
  • no support for reading VB for opendocuments

Credits

Thanks to xlsx-js developers! This library is by far the simplest open source implementation I could find and helps making sense out of official documentation.

Thanks also to all the contributors!

License

MIT

About

A pure Rust Excel/OpenDocument SpreadSheets file reader: rust on metal sheets

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

calamine

An Excel/OpenDocument Spreadsheets file reader/deserializer, in pure Rust.

GitHub CI Rust testsBuild status

Documentation

Description

calamine is a pure Rust library to read and deserialize any spreadsheet file:

  • excel like (xls, xlsx, xlsm, xlsb, xla, xlam)
  • opendocument spreadsheets (ods)

As long as your files are simple enough, this library should just work. For anything else, please file an issue with a failing test or send a pull request!

Examples

Serde deserialization

It is as simple as:

use calamine::{open_workbook,Error,Xlsx,Reader,RangeDeserializerBuilder};fnexample() -> Result<(),Error>{let path = format!("{}/tests/temperature.xlsx", env!("CARGO_MANIFEST_DIR"));letmut workbook:Xlsx<_> = open_workbook(path)?;let range = workbook.worksheet_range("Sheet1").ok_or(Error::Msg("Cannot find 'Sheet1'"))??;letmut iter = RangeDeserializerBuilder::new().from_range(&range)?;ifletSome(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"))}}

Note if you want to deserialize a column that may have invalid types (i.e. a float where some values may be strings), you can use Serde's deserialize_with field attribute:

use serde::Deserialize;use calamine::{RangeDeserializerBuilder,Reader,Xlsx};#[derive(Deserialize)]structExcelRow{metric:String,#[serde(deserialize_with = "de_opt_f64")]value:Option<f64>,}// Convert value cell to Some(f64) if float or int, else Nonefnde_opt_f64<'de,D>(deserializer:D) -> Result<Option<f64>,D::Error>whereD: serde::Deserializer<'de>,{let data_type = calamine::DataType::deserialize(deserializer)?;ifletSome(float) = data_type.as_f64(){Ok(Some(float))}else{Ok(None)}}fnmain() -> Result<(),Box<dyn std::error::Error>>{let path = format!("{}/tests/excel.xlsx", env!("CARGO_MANIFEST_DIR"));letmut excel:Xlsx<_> = open_workbook(path)?;let range = excel
.worksheet_range("Sheet1").ok_or(calamine::Error::Msg("Cannot find Sheet1"))??;let iter_result =
RangeDeserializerBuilder::with_headers(&COLUMNS).from_range::<_,ExcelRow>(&range)?;}

Reader: Simple

use calamine::{Reader,Xlsx, open_workbook};letmut excel:Xlsx<_> = open_workbook("file.xlsx").unwrap();ifletSome(Ok(r)) = excel.worksheet_range("Sheet1"){for row in r.rows(){println!("row={:?}, row[0]={:?}", row, row[0]);}}

Reader: More complex

Let's assume

  • the file type (xls, xlsx ...) cannot be known at static time
  • we need to get all data from the workbook
  • we need to parse the vba
  • we need to see the defined names
  • and the formula!
use calamine::{Reader, open_workbook_auto,Xlsx,DataType};// opens a new workbooklet path = ...;// we do not know the file typeletmut workbook = open_workbook_auto(path).expect("Cannot open file");// Read whole worksheet data and provide some statisticsifletSome(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 rowsassert_eq!(non_empty_cells, range.rows().flat_map(|r| r.iter().filter(|&c| c != &DataType::Empty)).count());}// Check if the workbook has a vba projectifletSome(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);}

Features

  • dates: Add date related fn to DataType.
  • picture: Extract picture data.

Others

Browse the examples directory.

Performance

As calamine is readonly, the comparisons will only involve reading an excel xlsx file and then iterating over the rows. Along with calamine, three other libraries were chosen, from three different languages:

The benchmarks were done using this dataset, a 186MBxlsx file when the csv is converted. The plotting data was gotten from the sysinfo crate, at a sample interval of 200ms. The program samples the reported values for the running process and records it.

The programs are all structured to follow the same constructs:

calamine:

use calamine::{open_workbook,Reader,Xlsx};fnmain(){// Open workbook letmut excel:Xlsx<_> =
open_workbook("NYC_311_SR_2010-2020-sample-1M.xlsx").expect("failed to find file");// Get worksheetlet sheet = excel
.worksheet_range("NYC_311_SR_2010-2020-sample-1M").unwrap().unwrap();// iterate over rowsfor _row in sheet.rows(){}}

excelize:

package main
import (
"fmt""github.com/xuri/excelize/v2"
)
funcmain() {
// Open workbookfile, err:=excelize.OpenFile(`NYC_311_SR_2010-2020-sample-1M.xlsx`)
iferr!=nil {
fmt.Println(err)
return
}
deferfunc() {
// Close the spreadsheet.iferr:=file.Close(); err!=nil {
fmt.Println(err)
}
}()
// Select worksheetrows, err:=file.Rows("NYC_311_SR_2010-2020-sample-1M")
iferr!=nil {
fmt.Println(err)
return
}
// Iterate over rowsforrows.Next() {
}
}

ClosedXML:

usingClosedXML.Excel;internalclassProgram{privatestaticvoidMain(string[]args){// Open workbookusingvarworkbook=newXLWorkbook("NYC_311_SR_2010-2020-sample-1M.xlsx");// Get Worksheet// "NYC_311_SR_2010-2020-sample-1M"varworksheet=workbook.Worksheet(1);// Iterate over rowsforeach(varrowinworksheet.Rows()){}}}

openpyxl:

fromopenpyxlimportload_workbook# Open workbookwb=load_workbook(
filename=r'NYC_311_SR_2010-2020-sample-1M.xlsx', read_only=True)
# Get worksheetws=wb['NYC_311_SR_2010-2020-sample-1M']
# Iterate over rowsforrowinws.rows:
_=row# Close the workbook after readingwb.close()

Benchmarks

The benchmarking was done using hyperfine with --warmup 3 on an AMD RYZEN 9 5900X @ 4.0GHz running Windows 11. Both calamine and ClosedXML were built in release mode.

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 is 1.75x faster than excelize, 7.05x faster than ClosedXML, and 9.43x faster than openpyxl.

The spreadsheet has a range of 1,000,001 rows and 41 columns, for a total of 41,000,041 cells in the range. Of those, 28,056,975 cells had values.

Going off of that number:

  • calamine => 1,122,279 cells per second
  • excelize => 633,998 cells per second
  • ClosedXML => 157,320 cells per second
  • openpyxl => 117,612 cells per second

Plots

Disk Read

bytes_from_disk

As stated, the filesize on disk is 186MB:

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

When asking one of the maintainers of excelize, I got this response:

To avoid high memory usage for reading large files, this library allows user-specific UnzipXMLSizeLimit options when opening the workbook, to set the memory limit on the unzipping worksheet and shared string table in bytes, worksheet XML will be extracted to the system temporary directory when the file size is over this value, so you can see that data written in reading mode, and you can change the default for that to avoid this behavior.

- xuri

Disk Write

bytes_to_disk

As seen in the previous section, excelize is writting to disk to save memory. The others don't employ that kind of mechanism.

Memory

mem_usage

virt_mem_usage

Note

ClosedXML was reporting a constant 2.5TB of virtual memory usage, so it was excluded from the chart.

The stepping and falling for calamine is from the grows of Vecs and the freeing of memory right after, with the memory usage dropping down again. The sudden jump at the end is when the sheet is being read into memory. The others, being garbage collected, have a more linear climb all the way through.

CPU

cpu_usage

Very noisy chart, but excelize's spikes must be from the GC?

Unsupported

Many (most) part of the specifications are not implemented, the focus has been put on reading cell values and vba code.

The main unsupported items are:

  • no support for writing excel files, this is a read-only library
  • no support for reading extra contents, such as formatting, excel parameter, encrypted components etc ...
  • no support for reading VB for opendocuments

Credits

Thanks to xlsx-js developers! This library is by far the simplest open source implementation I could find and helps making sense out of official documentation.

Thanks also to all the contributors!

License

MIT

About

A pure Rust Excel/OpenDocument SpreadSheets file reader: rust on metal sheets

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

calamine

An Excel/OpenDocument Spreadsheets file reader/deserializer, in pure Rust.

GitHub CI Rust testsBuild status

Documentation

Description

calamine is a pure Rust library to read and deserialize any spreadsheet file:

  • excel like (xls, xlsx, xlsm, xlsb, xla, xlam)
  • opendocument spreadsheets (ods)

As long as your files are simple enough, this library should just work. For anything else, please file an issue with a failing test or send a pull request!

Examples

Serde deserialization

It is as simple as:

use calamine::{open_workbook,Error,Xlsx,Reader,RangeDeserializerBuilder};fnexample() -> Result<(),Error>{let path = format!("{}/tests/temperature.xlsx", env!("CARGO_MANIFEST_DIR"));letmut workbook:Xlsx<_> = open_workbook(path)?;let range = workbook.worksheet_range("Sheet1").ok_or(Error::Msg("Cannot find 'Sheet1'"))??;letmut iter = RangeDeserializerBuilder::new().from_range(&range)?;ifletSome(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"))}}

Note if you want to deserialize a column that may have invalid types (i.e. a float where some values may be strings), you can use Serde's deserialize_with field attribute:

use serde::Deserialize;use calamine::{RangeDeserializerBuilder,Reader,Xlsx};#[derive(Deserialize)]structExcelRow{metric:String,#[serde(deserialize_with = "de_opt_f64")]value:Option<f64>,}// Convert value cell to Some(f64) if float or int, else Nonefnde_opt_f64<'de,D>(deserializer:D) -> Result<Option<f64>,D::Error>whereD: serde::Deserializer<'de>,{let data_type = calamine::DataType::deserialize(deserializer)?;ifletSome(float) = data_type.as_f64(){Ok(Some(float))}else{Ok(None)}}fnmain() -> Result<(),Box<dyn std::error::Error>>{let path = format!("{}/tests/excel.xlsx", env!("CARGO_MANIFEST_DIR"));letmut excel:Xlsx<_> = open_workbook(path)?;let range = excel
.worksheet_range("Sheet1").ok_or(calamine::Error::Msg("Cannot find Sheet1"))??;let iter_result =
RangeDeserializerBuilder::with_headers(&COLUMNS).from_range::<_,ExcelRow>(&range)?;}

Reader: Simple

use calamine::{Reader,Xlsx, open_workbook};letmut excel:Xlsx<_> = open_workbook("file.xlsx").unwrap();ifletSome(Ok(r)) = excel.worksheet_range("Sheet1"){for row in r.rows(){println!("row={:?}, row[0]={:?}", row, row[0]);}}

Reader: More complex

Let's assume

  • the file type (xls, xlsx ...) cannot be known at static time
  • we need to get all data from the workbook
  • we need to parse the vba
  • we need to see the defined names
  • and the formula!
use calamine::{Reader, open_workbook_auto,Xlsx,DataType};// opens a new workbooklet path = ...;// we do not know the file typeletmut workbook = open_workbook_auto(path).expect("Cannot open file");// Read whole worksheet data and provide some statisticsifletSome(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 rowsassert_eq!(non_empty_cells, range.rows().flat_map(|r| r.iter().filter(|&c| c != &DataType::Empty)).count());}// Check if the workbook has a vba projectifletSome(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);}

Features

  • dates: Add date related fn to DataType.
  • picture: Extract picture data.

Others

Browse the examples directory.

Performance

As calamine is readonly, the comparisons will only involve reading an excel xlsx file and then iterating over the rows. Along with calamine, three other libraries were chosen, from three different languages:

The benchmarks were done using this dataset, a 186MBxlsx file when the csv is converted. The plotting data was gotten from the sysinfo crate, at a sample interval of 200ms. The program samples the reported values for the running process and records it.

The programs are all structured to follow the same constructs:

calamine:

use calamine::{open_workbook,Reader,Xlsx};fnmain(){// Open workbook letmut excel:Xlsx<_> =
open_workbook("NYC_311_SR_2010-2020-sample-1M.xlsx").expect("failed to find file");// Get worksheetlet sheet = excel
.worksheet_range("NYC_311_SR_2010-2020-sample-1M").unwrap().unwrap();// iterate over rowsfor _row in sheet.rows(){}}

excelize:

package main
import (
"fmt""github.com/xuri/excelize/v2"
)
funcmain() {
// Open workbookfile, err:=excelize.OpenFile(`NYC_311_SR_2010-2020-sample-1M.xlsx`)
iferr!=nil {
fmt.Println(err)
return
}
deferfunc() {
// Close the spreadsheet.iferr:=file.Close(); err!=nil {
fmt.Println(err)
}
}()
// Select worksheetrows, err:=file.Rows("NYC_311_SR_2010-2020-sample-1M")
iferr!=nil {
fmt.Println(err)
return
}
// Iterate over rowsforrows.Next() {
}
}

ClosedXML:

usingClosedXML.Excel;internalclassProgram{privatestaticvoidMain(string[]args){// Open workbookusingvarworkbook=newXLWorkbook("NYC_311_SR_2010-2020-sample-1M.xlsx");// Get Worksheet// "NYC_311_SR_2010-2020-sample-1M"varworksheet=workbook.Worksheet(1);// Iterate over rowsforeach(varrowinworksheet.Rows()){}}}

openpyxl:

fromopenpyxlimportload_workbook# Open workbookwb=load_workbook(
filename=r'NYC_311_SR_2010-2020-sample-1M.xlsx', read_only=True)
# Get worksheetws=wb['NYC_311_SR_2010-2020-sample-1M']
# Iterate over rowsforrowinws.rows:
_=row# Close the workbook after readingwb.close()

Benchmarks

The benchmarking was done using hyperfine with --warmup 3 on an AMD RYZEN 9 5900X @ 4.0GHz running Windows 11. Both calamine and ClosedXML were built in release mode.

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 is 1.75x faster than excelize, 7.05x faster than ClosedXML, and 9.43x faster than openpyxl.

The spreadsheet has a range of 1,000,001 rows and 41 columns, for a total of 41,000,041 cells in the range. Of those, 28,056,975 cells had values.

Going off of that number:

  • calamine => 1,122,279 cells per second
  • excelize => 633,998 cells per second
  • ClosedXML => 157,320 cells per second
  • openpyxl => 117,612 cells per second

Plots

Disk Read

bytes_from_disk

As stated, the filesize on disk is 186MB:

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

When asking one of the maintainers of excelize, I got this response:

To avoid high memory usage for reading large files, this library allows user-specific UnzipXMLSizeLimit options when opening the workbook, to set the memory limit on the unzipping worksheet and shared string table in bytes, worksheet XML will be extracted to the system temporary directory when the file size is over this value, so you can see that data written in reading mode, and you can change the default for that to avoid this behavior.

- xuri

Disk Write

bytes_to_disk

As seen in the previous section, excelize is writting to disk to save memory. The others don't employ that kind of mechanism.

Memory

mem_usage

virt_mem_usage

Note

ClosedXML was reporting a constant 2.5TB of virtual memory usage, so it was excluded from the chart.

The stepping and falling for calamine is from the grows of Vecs and the freeing of memory right after, with the memory usage dropping down again. The sudden jump at the end is when the sheet is being read into memory. The others, being garbage collected, have a more linear climb all the way through.

CPU

cpu_usage

Very noisy chart, but excelize's spikes must be from the GC?

Unsupported

Many (most) part of the specifications are not implemented, the focus has been put on reading cell values and vba code.

The main unsupported items are:

  • no support for writing excel files, this is a read-only library
  • no support for reading extra contents, such as formatting, excel parameter, encrypted components etc ...
  • no support for reading VB for opendocuments

Credits

Thanks to xlsx-js developers! This library is by far the simplest open source implementation I could find and helps making sense out of official documentation.

Thanks also to all the contributors!

License

MIT

About

A pure Rust Excel/OpenDocument SpreadSheets file reader: rust on metal sheets

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

calamine

An Excel/OpenDocument Spreadsheets file reader/deserializer, in pure Rust.

GitHub CI Rust testsBuild status

Documentation

Description

calamine is a pure Rust library to read and deserialize any spreadsheet file:

  • excel like (xls, xlsx, xlsm, xlsb, xla, xlam)
  • opendocument spreadsheets (ods)

As long as your files are simple enough, this library should just work. For anything else, please file an issue with a failing test or send a pull request!

Examples

Serde deserialization

It is as simple as:

use calamine::{open_workbook,Error,Xlsx,Reader,RangeDeserializerBuilder};fnexample() -> Result<(),Error>{let path = format!("{}/tests/temperature.xlsx", env!("CARGO_MANIFEST_DIR"));letmut workbook:Xlsx<_> = open_workbook(path)?;let range = workbook.worksheet_range("Sheet1").ok_or(Error::Msg("Cannot find 'Sheet1'"))??;letmut iter = RangeDeserializerBuilder::new().from_range(&range)?;ifletSome(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"))}}

Note if you want to deserialize a column that may have invalid types (i.e. a float where some values may be strings), you can use Serde's deserialize_with field attribute:

use serde::Deserialize;use calamine::{RangeDeserializerBuilder,Reader,Xlsx};#[derive(Deserialize)]structExcelRow{metric:String,#[serde(deserialize_with = "de_opt_f64")]value:Option<f64>,}// Convert value cell to Some(f64) if float or int, else Nonefnde_opt_f64<'de,D>(deserializer:D) -> Result<Option<f64>,D::Error>whereD: serde::Deserializer<'de>,{let data_type = calamine::DataType::deserialize(deserializer)?;ifletSome(float) = data_type.as_f64(){Ok(Some(float))}else{Ok(None)}}fnmain() -> Result<(),Box<dyn std::error::Error>>{let path = format!("{}/tests/excel.xlsx", env!("CARGO_MANIFEST_DIR"));letmut excel:Xlsx<_> = open_workbook(path)?;let range = excel
.worksheet_range("Sheet1").ok_or(calamine::Error::Msg("Cannot find Sheet1"))??;let iter_result =
RangeDeserializerBuilder::with_headers(&COLUMNS).from_range::<_,ExcelRow>(&range)?;}

Reader: Simple

use calamine::{Reader,Xlsx, open_workbook};letmut excel:Xlsx<_> = open_workbook("file.xlsx").unwrap();ifletSome(Ok(r)) = excel.worksheet_range("Sheet1"){for row in r.rows(){println!("row={:?}, row[0]={:?}", row, row[0]);}}

Reader: More complex

Let's assume

  • the file type (xls, xlsx ...) cannot be known at static time
  • we need to get all data from the workbook
  • we need to parse the vba
  • we need to see the defined names
  • and the formula!
use calamine::{Reader, open_workbook_auto,Xlsx,DataType};// opens a new workbooklet path = ...;// we do not know the file typeletmut workbook = open_workbook_auto(path).expect("Cannot open file");// Read whole worksheet data and provide some statisticsifletSome(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 rowsassert_eq!(non_empty_cells, range.rows().flat_map(|r| r.iter().filter(|&c| c != &DataType::Empty)).count());}// Check if the workbook has a vba projectifletSome(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);}

Features

  • dates: Add date related fn to DataType.
  • picture: Extract picture data.

Others

Browse the examples directory.

Performance

As calamine is readonly, the comparisons will only involve reading an excel xlsx file and then iterating over the rows. Along with calamine, three other libraries were chosen, from three different languages:

The benchmarks were done using this dataset, a 186MBxlsx file when the csv is converted. The plotting data was gotten from the sysinfo crate, at a sample interval of 200ms. The program samples the reported values for the running process and records it.

The programs are all structured to follow the same constructs:

calamine:

use calamine::{open_workbook,Reader,Xlsx};fnmain(){// Open workbook letmut excel:Xlsx<_> =
open_workbook("NYC_311_SR_2010-2020-sample-1M.xlsx").expect("failed to find file");// Get worksheetlet sheet = excel
.worksheet_range("NYC_311_SR_2010-2020-sample-1M").unwrap().unwrap();// iterate over rowsfor _row in sheet.rows(){}}

excelize:

package main
import (
"fmt""github.com/xuri/excelize/v2"
)
funcmain() {
// Open workbookfile, err:=excelize.OpenFile(`NYC_311_SR_2010-2020-sample-1M.xlsx`)
iferr!=nil {
fmt.Println(err)
return
}
deferfunc() {
// Close the spreadsheet.iferr:=file.Close(); err!=nil {
fmt.Println(err)
}
}()
// Select worksheetrows, err:=file.Rows("NYC_311_SR_2010-2020-sample-1M")
iferr!=nil {
fmt.Println(err)
return
}
// Iterate over rowsforrows.Next() {
}
}

ClosedXML:

usingClosedXML.Excel;internalclassProgram{privatestaticvoidMain(string[]args){// Open workbookusingvarworkbook=newXLWorkbook("NYC_311_SR_2010-2020-sample-1M.xlsx");// Get Worksheet// "NYC_311_SR_2010-2020-sample-1M"varworksheet=workbook.Worksheet(1);// Iterate over rowsforeach(varrowinworksheet.Rows()){}}}

openpyxl:

fromopenpyxlimportload_workbook# Open workbookwb=load_workbook(
filename=r'NYC_311_SR_2010-2020-sample-1M.xlsx', read_only=True)
# Get worksheetws=wb['NYC_311_SR_2010-2020-sample-1M']
# Iterate over rowsforrowinws.rows:
_=row# Close the workbook after readingwb.close()

Benchmarks

The benchmarking was done using hyperfine with --warmup 3 on an AMD RYZEN 9 5900X @ 4.0GHz running Windows 11. Both calamine and ClosedXML were built in release mode.

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 is 1.75x faster than excelize, 7.05x faster than ClosedXML, and 9.43x faster than openpyxl.

The spreadsheet has a range of 1,000,001 rows and 41 columns, for a total of 41,000,041 cells in the range. Of those, 28,056,975 cells had values.

Going off of that number:

  • calamine => 1,122,279 cells per second
  • excelize => 633,998 cells per second
  • ClosedXML => 157,320 cells per second
  • openpyxl => 117,612 cells per second

Plots

Disk Read

bytes_from_disk

As stated, the filesize on disk is 186MB:

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

When asking one of the maintainers of excelize, I got this response:

To avoid high memory usage for reading large files, this library allows user-specific UnzipXMLSizeLimit options when opening the workbook, to set the memory limit on the unzipping worksheet and shared string table in bytes, worksheet XML will be extracted to the system temporary directory when the file size is over this value, so you can see that data written in reading mode, and you can change the default for that to avoid this behavior.

- xuri

Disk Write

bytes_to_disk

As seen in the previous section, excelize is writting to disk to save memory. The others don't employ that kind of mechanism.

Memory

mem_usage

virt_mem_usage

Note

ClosedXML was reporting a constant 2.5TB of virtual memory usage, so it was excluded from the chart.

The stepping and falling for calamine is from the grows of Vecs and the freeing of memory right after, with the memory usage dropping down again. The sudden jump at the end is when the sheet is being read into memory. The others, being garbage collected, have a more linear climb all the way through.

CPU

cpu_usage

Very noisy chart, but excelize's spikes must be from the GC?

Unsupported

Many (most) part of the specifications are not implemented, the focus has been put on reading cell values and vba code.

The main unsupported items are:

  • no support for writing excel files, this is a read-only library
  • no support for reading extra contents, such as formatting, excel parameter, encrypted components etc ...
  • no support for reading VB for opendocuments

Credits

Thanks to xlsx-js developers! This library is by far the simplest open source implementation I could find and helps making sense out of official documentation.

Thanks also to all the contributors!

License

MIT

About

A pure Rust Excel/OpenDocument SpreadSheets file reader: rust on metal sheets

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages