read-excel-file
read-excel-file
Read .xlsx files in a browser or Node.js.
Also check out write-excel-file for writing .xlsx files.
Migrating from 6.x to 7.x
- Renamed the default export
"read-excel-file"to"read-excel-file/browser", and it uses Web Workers now.- Old:
import readExcelFile from "read-excel-file" - New:
import readExcelFile from "read-excel-file/browser"
- Old:
- The minimum required Node.js version is 18.
Migrating from 7.x to 8.x
If you were using the default exported function:
- Renamed the default exported function to a named exported function
readSheet.- Old:
import readExcelFile from "read-excel-file/browser" - New:
import { readSheet } from "read-excel-file/browser" - And same for other exports like
"read-excel-file/node", etc.
- Old:
- The default exported function now returns a different kind of result. Specifically, now it returns all available sheets — an array of objects:
[{ sheet: "Sheet 1", data: [['a1','b1','c1'],['a2','b2','c2']] }, ...]. - The default exported function used to return sheet names when passed
getSheets: trueparameter. Now, instead of that, the default exported function just returns all available sheets, from which one could get the sheet names.
- Renamed the default exported function to a named exported function
If you were using
readSheetNames()function:- Removed exported function
readSheetNames(). Use the default exported function instead. The default exported function now returns all sheets.
- Removed exported function
If you were using
parseExcelDate()function:- Removed exported function
parseExcelDate()because there seems to be no need to have it exported.
- Removed exported function
If you were using
schemaparameter:- Removed
schemaparameter. Instead, use exported functionparseData(data, schema)to map data to an array of objects.- Old:
import readXlsxFile from "read-excel-file"and thenconst { rows, errors } = await readXlsxFile(..., { schema }) - New:
import { readSheet, parseData } from "read-excel-file/browser"and thenconst result = parseData(await readSheet(...), schema)- The
resultof the function is an array where each element represents a "data row" and has shape{ object, errors }.- Depending on whether there were any errors when parsing a given "data row", either
objectorerrorsproperty will beundefined. - The
errorsdon't have arowproperty anymore because it could be derived from "data row" number.- In version
9.x, therowproperty has been re-added, so consider migrating straight to9.x.
- In version
- In version
9.x, the returned result ofparseData()has been changed back to{ errors, objects }, so consider migrating straight to9.x. In that case, if there're no errors,errorswill beundefined; otherwise,errorswill be a non-empty array andobjectswill beundefined. - In version
9.x, theschemaparameter was re-added toreadSheet()function, so consider migrating straight to9.x.
- Depending on whether there were any errors when parsing a given "data row", either
- The
- Old:
- Renamed some
schema-related parameters:schemaPropertyValueForMissingColumn→propertyValueWhenColumnIsMissingschemaPropertyValueForMissingValue→propertyValueWhenCellIsEmptyschemaPropertyShouldSkipRequiredValidationForMissingColumn→ (removed)getEmptyObjectValue→transformEmptyObject- The leading
.character is now removed from thepathparameter.
- The leading
getEmptyArrayValue→transformEmptyArray- The leading
.character is now removed from thepathparameter.
- The leading
- Previously, when using a
schemato parse comma-separated values, it used to ignore any commas that're surrounded by quotes, similar to how it's done in.csvfiles. Now it no longer does that. - Previously, when using a
schemato parse comma-separated values, it used to allow empty-string elements. Now it no longer does that and such empty-string elements will now result in an error with properties:{ error: "invalid", reason: "syntax" }. - Previously, when using a
schemato parsetype: Dateproperties, it used to support bothDateobjects and numeric timestamps as the input data for the property value. In the latter case, it simply force-converted those numeric timestamps to correspondingDateobjects. NowparseData()function no longer does that, and demands the input data fortype: Dateschema properties to only beDateobjects, i.e. it shifts the responsibility to interpret date cell values correctly ontoreadSheet()andreadExcelFile()functions. And I'd personally assume that in any real-world (i.e. non-contrived) scenario those functions would interpret date cell values correctly, so I personally don't consider this a "breaking change". Still, formally, it is a "breaking change" and therefore should be mentioned. So if, for some strange reason, those two functions happen to not recognize a date cell value correctly,parseData()function will return an error for such cell:"not_a_date". - Previously, when using a
schemato parse sheet data, and a given row of data was completely empty, it didn't run anyrequiredproperty validations. Now it no longer does that and it will run allrequiredproperty validations regardless of whether it's a completely empty row of data or not.
- Removed
If you were using
transformDataparameter:- Removed
transformDataparameter because theschemaparameter was extracted into a separate function calledparseData(). Now, if required, a developer could transform thedatamanually and then pass it toparseData()function.
- Removed
If you were using
isColumnOrientedparameter:- Removed
isColumnOrientedparameter because it seemed to be of no use.
- Removed
If you were using
ignoreEmptyRowsparameter:- Removed
ignoreEmptyRowsparameter. PassingignoreEmptyRows: trueparameter no longer makes it skip empty rows in the middle of a sheet. Now it's always the default behavior, as it used to be: only empty rows at the end of a sheet are ignored.
- Removed
If you were using TypeScript:
- Renamed some of the exported types:
Type→ParseDataCustomTypeErrororSchemaParseCellValueError→ParseDataErrorCellValueRequiredError→ParseDataValueRequiredErrorParsedObjectsResult→ParseDataResult
- Renamed some of the exported types:
Migrating from 8.x to 9.x
- If you were using
parseData()function:- Rewrote the code of the
parseData()function and renamed it toparseSheetData(). - The result of
parseSheetData()function is now{ errors, objects }. If there're no errors,errorswill beundefined. Otherwise,errorswill be a non-empty array andobjectswill beundefined.- Previously the result of
parseSheetData()function was[{ errors, object }, ...], i.e. theerrorswere split between each particular data row. Now theerrorsare combined for all data rows. The rationale is that it's simpler to handle the result of the function this way. - Re-added
row: numberproperty to theerrorobject. It's the number of the data row that caused the error, starting from1. - Added
columnIndex: numberproperty to theerrorobject.
- Previously the result of
- Renamed some of the exported TypeScript types:
ParseDataCustomType→ParseSheetDataCustomTypeParseDataCustomTypeErrorMessage→ParseSheetDataCustomTypeErrorMessageParseDataCustomTypeErrorReason→ParseSheetDataCustomTypeErrorReasonParseDataError→ParseSheetDataErrorParseDataValueRequiredError→ParseSheetDataValueRequiredErrorParseDataResult→ParseSheetDataResult
- In a
schema, a nested object could be declared as:{ required: true/false, schema: { ... } }. This is still true but therequiredflag is now only allowed to be eitherundefinedorfalse, sotruevalue is not allowed. The reason is quite simple. If a nested object as a whole is marked asrequired: true, and then it happens to be empty, a"required"error should be returned for it. But that error would also have to include acolumntitle, and a nested object simply can't be pinned down to a single column in a sheet because it is by definition spread over multiple columns. So instead of marking a nested object as a whole withrequired: true, mark the specific required properties of it. - Re-added
schemaparameter toreadSheet()function.const { objects, errors } = readSheet(data, { schema })
- Rewrote the code of the
Install
npm install read-excel-file
Alternatively, it could be included on a web page directly via a <script/> tag.
How to Use
Suppose there's an .xlsx file:
| Name | Date of Birth | Married | Kids |
|---|---|---|---|
| John Smith | 1/1/1995 | TRUE | 3 |
| Kate Brown | 3/1/2010 | FALSE | 0 |
That file could be represented as an array of rows, each row being an array of cells — they call it a "2D array":
[
['Name', 'Date of Birth', 'Married', 'Kids'],
['John Smith', 1995-01-01T00:00:00.000Z, true, 3],
['Kate Brown', 2010-03-01T00:00:00.000Z, false, 0]
]
Or it could be represented as an array of JSON objects:
[
{
name: 'John Smith',
dateOfBirth: 1995-01-01T00:00:00.000Z,
married: true,
kids: 3
},
{
name: 'Kate Brown',
dateOfBirth: 2010-03-01T00:00:00.000Z,
married: false,
kids: 0
}
]
So we could read it both ways.
First, we'll look at the first approach, then the second.
Read as 2D Array
Let's say you prefer reading an .xlsx file as a 2D array.
Here's how to do it using readSheet() function:
import { readSheet } from 'read-excel-file/node'
await readSheet(file) ===
[
['Name', 'Date of Birth', 'Married', 'Kids'],
['John Smith', 1995-01-01T00:00:00.000Z, true, 3],
['Kate Brown', 2010-03-01T00:00:00.000Z, false, 0]
]
* We import the function from read-excel-file/node because we plan to run it in Node.js. See all supported environments.
The result is an array of rows. Each row is an array of values — string, number, boolean or Date.
If an .xlsx file has multiple sheets, then you could pass a second argument — sheet — which could be a sheet number (starting from 1) or a sheet name. By default, it always reads the first sheet.
If an .xlsx file has multiple sheets and you need to read all of them, use the default exported function instead of readSheet().
Example: Reading all sheets.
import readExcelFile from 'read-excel-file/node'
await readExcelFile(file) ===
[{
sheet: 'Sheet1',
data: [
['Name', ...],
['John Smith', ...],
['Kate Brown', ...]
]
}, {
sheet: 'Sheet2',
data: ...
}]
Read as Objects
Reading an .xlsx file as a 2D array is perfectly fine for a machine or an AI agent but not really convenient for a human. Humans are more comfortable accessing the data in the form of JSON objects with named properties rather than just "raw" 2D arrays.
So let's say that a sheet that you'd like to read has a simple "table" structure:
- The first row is a "header" row (column titles)
- The other rows are "data" rows (values for those columns)
Like this:
| Name | Date of Birth | Married | Kids |
|---|---|---|---|
| John Smith | 1/1/1995 | TRUE | 3 |
| Kate Brown | 3/1/2010 | FALSE | 0 |
Then you just define a "schema" for the desired JSON object, i.e. you outline the shape of the desired object, telling it to pull each property from a specific column. Like this:
const schema = {
name: 'Name',
dateOfBirth: 'Date of Birth',
married: 'Married',
kids: 'Kids'
}
And then pass that schema to the readSheet() function, making it return the data as JSON objects:
const { objects } = await readSheet(file, { schema })
objects === [
{
name: 'John Smith',
dateOfBirth: 1995-01-01T00:00:00.000Z,
married: true,
kids: 3
},
{
name: 'Kate Brown',
dateOfBirth: 2010-03-01T00:00:00.000Z,
married: false,
kids: 0
}
]
There're also a few more "advanced" features to schema such as: support for nested objects, required property validation, value type validation, etc. Read more if you're interested.
Import
This package provides a separate import path for each different environment, as described below.
read-excel-file/browser
Reads an .xlsx file in a browser from a File, a Blob or an ArrayBuffer.
Example 1: Read from a selected file.
<input type="file" id="input" />
import { readSheet } from 'read-excel-file/browser'
const input = document.getElementById('input')
input.addEventListener('change', () => {
const data = await readSheet(event.target.files[0])
})
Example 2: Read from a URL.
import { readSheet } from 'read-excel-file/browser'
const response = await fetch('https://example.com/spreadsheet.xlsx')
const blob = await response.blob()
const data = await readSheet(blob)
read-excel-file/node
Reads an .xlsx file in Node.js from a file path, a Stream, a Buffer or a Blob.
Example 1: Read from a file path.
import { readSheet } from 'read-excel-file/node'
const data = await readSheet('/path/to/file')
Example 2: Read from a Stream
import { readSheet } from 'read-excel-file/node'
const data = await readSheet(fs.createReadStream('/path/to/file'))
read-excel-file/universal
Reads an .xlsx file in any supported environment, such as a browser or Node.js, but comes with a limitation: it can only read from a Blob or an ArrayBuffer, which could be a bit less convenient for general use.
import { readSheet } from 'read-excel-file/universal'
const data = await readSheet(blob)
Strings
By default, text cell values will be trimmed. To prevent that, pass trim: false option.
readExcelFile(file, { trim: false })
Dates
.xlsx file format has no type for dates. It simply stores dates as regular numbers, and then assigns a date-specific formatting template to those numbers. So telling a number from a date is not always a trivial task, but so far it seems to work correctly.
Numbers
When reading an .xlsx file, any numeric values are parsed from a string to a javascript number. And that works for everyone, except when you work in science or finance or banking where numbers absolutely need to be 100% precise, in which case this section is for you, otherwise don't even bother reading it.
Why javascript numbers aren't 100% precise
"So aren't javascript numbers already 100% precise?", you ask. Here're some rather contrived examples when they aren't:
1.0000000000000001will be rounded to188259496234518.57will be rounded to88259496234518.5699999999999999999999will be rounded to100000000000000000000
You see, javascript numbers inherently come with a limited floating-point precision, which is apparently not enough in the examples shown above.
How to read 100% precise numbers
If you absolutely need to read any numbers with 100% precision, then you could tell it to use a custom implementation of "decimal" number type such as decimal.js instead of the standard javascript number type. Specifically, pass a custom parseNumber(string) function as an option when reading an .xlsx file, and it will parse any number from string exactly the way you tell it.
Example 1: Parse any numbers as "decimals", i.e. exactly as they are written in the .xlsx file.
import Decimal from 'decimal.js'
readExcelFile(file, {
parseNumber: (string) => new Decimal(string)
})
Example 2: Don't parse any numbers and just leave them as strings.
import Decimal from 'decimal.js'
readExcelFile(file, {
parseNumber: (string) => string
})
Formulas
When reading cells that use formulas to calculate their value, it doesn't really evaluate the formula. Instead, it "cheats" by returning the value that is already pre-computed by the spreadsheet editor application. And because this pre-computed value always exists, it just works.
Although I could hypothetically imagine a situation when a file is created not by a spreadsheet editor application, but rather by some hand-made script that doesn't respect this convention and doesn't bother pre-computing formulas, which is totally allowed by the specification, in which case it will simply read such cells as empty ones.
Finally, if a formula can't be evaluated due to an error, such as invalid syntax, or division by zero, or trying to add text to a number, or referenced row or column not found, etc, then such error will be ignored and the cell will be read as empty.
Errors
InvalidInputError
Sometimes people confuse .xlsx files with legacy binary .xls ones. The difference might be tricky to spot, so this package explicitly throws an InvalidInputError in such (and some other) cases.
name—"InvalidInputError"code— One of:"INPUT_TYPE_NOT_SUPPORTED"— The input argument is not of a supported type."XLS_FILE_NOT_SUPPORTED"— The input is a legacy binary.xlsfile (OLE2 Compound File Binary format), which is not supported. Such files should be re-saved in.xlsxformat in order to be readable by this package."FILE_NOT_SUPPORTED"— The input is neither.xlsxnor.xlsfile."INVALID_ZIP"— The input seems to be an.xlsxfile, and an.xlsxfile must be a valid ZIP archive, which it isn't."NO_DATA"— The input is empty.
InvalidSpreadsheetError
Will be thrown if there's something wrong with the .xlsx file contents while attempting to parse it.
name—"InvalidSpreadsheetError"
SheetNotFoundError
Will be thrown if a requested sheet doesn't exist.
name—"SheetNotFoundError"sheet— Sheet name or sheet numbersheets— Available sheet names
Schema
Mapping
In its most basic form, a schema just maps column titles to property names.
For example, to read a file dogs.xlsx:
| Name | Breed | Gender | Owner Name | Owner Phone | Owner Address |
|---|---|---|---|---|---|
| Rex | German Shepherd | M | John Smith | +12133734253 | East 26th Street |
| Luna | Siberian Husky | F | Kate Brown | +19177568000 | West 42nd Street |
One could use the following schema:
const schema = {
name: 'Name',
breed: 'Breed',
gender: 'Gender',
// the owner's info will be put in a nested object
owner: {
name: 'Owner Name',
phone: 'Owner Phone',
address: 'Owner Address'
}
}
Just pass it to readSheet() function:
const { objects } = await readSheet('dogs.xlsx', { schema })
And it will return the following objects:
objects === [
// objects[0]:
{
name: 'Rex',
breed: 'German Shepherd',
gender: 'M',
owner: {
name: 'John Smith',
phone: '+12133734253',
address: 'East 26th Street'
}
},
// objects[1]:
{
name: 'Luna',
breed: 'Siberian Husky',
gender: 'F',
owner: {
name: 'Kate Brown',
phone: '+19177568000',
address: 'West 42nd Street'
}
}
]
For a given column, the mapping algorithm is:
If a cell is not empty, cell value becomes the propery value.
If a cell is empty, the property is set to
null.- This can be overridden by passing
propertyValueWhenCellIsEmptyoption, which isnullby default.
- This can be overridden by passing
If the column is missing, the property is set to
undefined.- This can be overridden by passing
propertyValueWhenColumnIsMissingoption, which isundefinedby default.
- This can be overridden by passing
For any object:
- If all properties of such object are
undefinedornullthen the object itself will be set tonull.- This can be overridden by passing
transformEmptyObject(object, { path? })function as an option, which by default always returnsnull. - This rule applies both to the top-level object, and any of its nested objects.
- This can be overridden by passing
Validation
Aside from just mapping, there're also "advanced" features of schema such as data validation.
In that case, readSheet() returns an object of shape { objects, errors }:
- If there were any errors,
objectswill beundefinedanderrorswill be a list of errors. - If there were no errors,
errorswill beundefinedandobjectswill be a list of objects.
For example, suppose that we'd like to validate that all cell values have the correct type, and all required cells are filled in. All these validation rules will be specified in the schema:
const schema = {
name: {
column: 'Name',
type: String, // must be a string
required: true // is required
},
gender: {
column: 'Gender',
oneOf: ['M', 'F'] // must be "M" or "F"
},
dateOfBirth: {
column: 'Date of Birth',
type: Date // must be a date
}
}
In other words, where it has previously been just a column title, it's now an object with the following properties:
column— Column title.type— (optional) The expected type of the value.- Basic types:
StringNumberBooleanDate
- "Utility" types that're exported from this package:
IntegerEmailURL
- Custom type:
- A function that takes a cell value and returns any kind of a parsed value.
- If it returns
nullorundefined, no property value will be set. - It should throw an error if it finds the value invalid.
- Basic types:
oneOf— (optional) A list of possible values.validate(value)— (optional) A function that will be called on the parsed value, when the value is present. It should throw if it finds the value invalid.required— (optional) Is the value required? Could be one of:true— The column must exist and the cell value must not be empty.false— The column can be missing and the cell value can be empty.(object) => boolean— Could be required or not, depending on the other properties of the object. Must be resolved at runtime.
If it finds any errors when validating the data, the errors property returned from the function will be a non-empty array, with each error having properties:
error(string) — Error code."required"— when the property is required but the value is missing."invalid"— when the propertytypedoesn't match the observed cell value type."invalid"— when the value doesn't passoneOfvalidation.- If the column uses a custom
validatefunction, then theerrorwill be same as themessageof the error that was thrown by thevalidatefunction. - If the column uses a custom
type, then theerrorwill be same as themessageof the error that was thrown by thetype.
reason(string) — An optional secondary error code that provides some more details about the error. I.e. "error.errorhappened specifically because oferror.reason". Currently, it could only be returned from non-customtypes.- Example:
{ error: "invalid", reason: "not_a_number" }for atype: Numberproperty means that "the cell value is invalid because it's not a number".
- Example:
row(number) — Data row number, starting from1.row: 1means "first row of data", etc.- The header row is ignored.
column(string) — Column title.columnIndex(number) — Column index.columnIndex: 0means "first column", etc.
value— Cell value.type— Thetypeof the property, as defined in theschema.
TypeScript example
import type {
Schema,
CellValue,
ParseSheetDataError,
ParseSheetDataCustomType,
ParseSheetDataCustomTypeErrorMessage
} from 'read-excel-file/node'
type ColumnTitle = 'COLUMN TITLE 1' | 'COLUMN TITLE 2'
type CustomTypeValue = string
function CustomType(value: CellValue): CustomTypeValue {
if (typeof value !== 'string') {
throw new Error('not_a_string')
}
return '~' + value + '~'
}
type CustomTypeErrorMessage<Type extends ParseSheetDataCustomType<unknown>> =
Type extends typeof CustomType
? 'not_a_string'
: never
// type CustomTypeErrorReason<
// Type extends ParseSheetDataCustomType<unknown>,
// ErrorMessage extends ParseSheetDataCustomTypeErrorMessage<Type>
// > =
// Type extends typeof CustomType
// ? (ErrorMessage extends 'not_a_string' ? undefined : never)
// : never
type PossibleError = ParseSheetDataError<
ColumnTitle,
typeof CustomType,
CustomTypeErrorMessage<typeof CustomType>
// CustomTypeErrorReason<typeof CustomType, CustomTypeErrorMessage<typeof CustomType>>
>
interface Object {
property1: CustomTypeValue;
property2?: string;
}
const schema: Schema<Object, ColumnTitle> = {
property1: {
column: 'COLUMN TITLE 1',
type: CustomType,
required: true
},
property2: {
column: 'COLUMN TITLE 2',
type: String
}
}
const { objects, errors } = parseSheetData<Object, ColumnTitle, PossibleError>([
['COLUMN TITLE 1', 'COLUMN TITLE 2'],
['Value 1', 'Value 2']
], schema)
if (errors) {
for (const error of errors) {
console.error('Error in data row', error.row, 'column', error.column, ':', error.error, error.reason || '')
}
} else {
console.log('Objects', objects)
}
Validation of Nested Objects
A nested object can be marked as required: false. This will allow it to be absent from the sheet, even if some of its properties are defined as required: true.
For example, a schema could define an optional secondOwner nested object using required: false flag. If there's no second owner info, that's ok. But if there is, then the second owner's name must be specified, which is expressed by adding required: true flag on that property. This will guard against the cases when, for example, the second owner's phone is filled in their name is left blank — in such case it will throw an error.
const schema = {
name: { column: 'Name', type: String, required: true },
breed: { column: 'Breed', type: String },
owner: {
name: { column: 'Owner Name', type: String, required: true },
phone: { column: 'Owner Phone', type: String }
},
secondOwner: {
required: false,
schema: {
name: { column: 'Second Owner Name', type: String, required: true },
phone: { column: 'Second Owner Phone', type: String }
}
}
}
Note how in order to be able to define a nested object as required: false, its structure has to be moved to a separate property called schema.
While a nested object can be defined as required: false, it can't be defined as required: true because it wouldn't really make sense — just mark the relevant properties of the nested object as required: true and that will make the nested object itself required as well.
Parsing Comma-Separated Values
If some column includes comma-separated values — such as "a, b, c" — then you could specify the type of the column as an array, and it will first split the value into substrings, and then parse each substring using the specified type.
For example, if a property is defined as { type: [String] } and the cell value is "a, b, c" then the property value will become ["a", "b", "c"].
Tips:
- If a cell is empty, or if every element of the parsed array is
nullorundefined, then the property value itself will be set tonull.- This can be overridden by passing
transformEmptyArray(array, { path })function as an option. By default, it returnsnull.
- This can be overridden by passing
- The separator could be specified by passing
arrayValueSeparatoroption. By default, it's",". - The separated parts will be trimmed.
Advanced Validation
This package adheres to the Unix-way philosophy in that it intentionally focuses on a single task — reading an .xlsx file — and doesn't wander too far. That's why it only includes the basic validation features, leaving the rest to specialized packages — such as zod — without leaning toward any particular one.
For example, one could first use read-excel-file with a schema to read the contents of an .xlsx file into an array of JSON objects, and then pass those JSON objects to something like zod for data validation. Decoupling validation logic from the task of reading an .xlsx file not only unties your hands in using advanced validation features but also opens new possibilities, such as reusing the validation logic on the server side for non-.xlsx input.
And in order to map those validation errors back to the spreadsheet's rows and columns, one could use the exported function called getColumnTitleForProperty(path, schema) which transforms a property path into a column title, such as: path: ["owner", "name"] → column: "Owner Name". A column title could then be transformed to column index by looking it up in the columns array that is returned from readSheet() function.
Example: using read-excel-file with zod
import { readSheet, getColumnTitleForProperty } from 'read-excel-file'
import * as z from 'zod'
// `read-excel-file` mapping schema
const schema = {
name: 'NAME',
age: 'AGE'
}
// `zod` schema, which repeats `read-excel-file` mapping schema
const Person = z.object({
name: z.string(),
age: z.number().optional().nullable()
})
// Read the contents of an `.xlsx` file into an array of JSON objects
const { objects, errors, columns } = await readSheet('persons.xlsx', { schema })
// If there were any errors while reading JSON objects from the `.xlsx` file
if (errors) {
// Report just the first error (for simplicity)
const { error: code, row, columnIndex, column: columnTitle } = errors[0]
reportError(code, row, columnIndex + 1, columnTitle)
}
// Reports an error
function reportError(code, row, column, columnTitle) {
throw new Error(`Row ${row}. Column ${column}. Column Title "${columnTitle}". Code: "${code}"`)
}
// Validate the JSON objects using `zod` schema
let row = 2 // skips the header row
for (const object of objects) {
try {
Person.parse(object)
} catch (error) {
if (error instanceof z.ZodError) {
// Convert `zod` error to `read-excel-file` error.
// Report just the first error (for simplicity)
const { code, path } = error.issues[0]
const columnTitle = getColumnTitleForProperty(path, schema)
if (columnTitle) {
const column = columns[columnTitle] + 1 // column number
reportError(code, row, column, columnTitle)
}
}
// For any other errors
throw error
}
row++
}
Standalone API
For convenience, this package exports the schema transformation feature as a separate function called parseSheetData(sheetData, schema).
import { readSheet, parseSheetData } from 'read-excel-file/node'
const schema = { ... }
const sheetData = await readSheet(file)
const { objects, errors } = parseSheetData(sheetData, schema)
if (errors) {
console.error(errors)
} else {
console.log(objects)
}
Performance
Here're the results of reading sample .xlsx files of different size:
| File Size | Browser | Node.js |
|---|---|---|
| 1 MB | 0.1 sec. | 0.1 sec. |
| 10 MB | 0.5 sec. | 0.5 sec. |
| 50 MB | 2.5 sec. | 2.5 sec. |
To run the benchmark in Node.js, clone the repository, download any sample .xlsx files to ./test/benchmark folder, run npm install and then npm run test:benchmark:node.
To run the benchmark in a web browser, open the demo page, open the console and choose an .xlsx file.
Performance tips
Reading an .xlsx file is performed in 3 steps:
- Step 1. Unzip an
.xlsxfile into a tree of.xmlfiles. - Step 2. Parse sheet data from those
.xmlfiles. - Step 3. If
schemaoption was passed, use it to transform sheet data rows into JSON objects.
When running in Node.js, the unzip step is outsourced to unzipper-esm and is "asynchronous" — it uses Node.js "native" zlib module which unzips data in a separate thread.
When running in a web browser, the unzip step is outsourced to fflate which does it "asynchronously" only for .xlsx files larger than 512 KB (the threshold is hardcoded in fflate code).
The XML parsing step is written using saxen which is a SAX parser. This step "synchronous".
The last step of transforming sheet data rows to JSON objects is also "synchronous".
Browser Support
An .xlsx file is just a .zip archive with an .xslx file extension. This package uses fflate for .zip decompression. See fflate's browser support for further details.
CDN
To include this library directly via a <script/> tag on a page, one can use any npm CDN service, e.g. unpkg.com or jsdelivr.com
<script src="https://unpkg.com/read-excel-file@[version]/bundle/read-excel-file.min.js"></script>
<script>
var input = document.getElementById('input')
input.addEventListener('change', function() {
readXlsxFile(event.target.files[0]).then(function(rows) {
// `rows` is an array of rows
// each row being an array of cells.
})
})
</script>
Dependencies
fflate— Unzips.ziparchives in web browsers.unzipper-esm— Unzips.ziparchives in Node.js usingstreamAPI.saxen— Parses XML in a streaming fashion.
Contributors
- Stian Jensen — Use
fflateunzipper on server side (1, 2) - Etienne Prothon — Reject non
.xlsxfiles, including the legacy binary.xlsfiles (1). Fix parsing of "encoded" characters (1). - Osama Ansar — Decode
_x-encoded strings (1)
GitHub
On March 9th, 2020, GitHub, Inc. silently banned my account (erasing all my repos, issues and comments, even in my employer's private repos) without any notice or explanation. Because of that, all source codes had to be promptly moved to GitLab. The GitHub repo is now only used as a backup (you can star the repo there too), and the primary repo is now the GitLab one. Issues can be reported in any repo.