Sheet.js正确读取excel中的日期

749 阅读3分钟

sheet.js可以很方便的在前端读取excel文件数据,一般读取文件后,每个单元格中的数据都会被解析为对象,后续我们可以使用该库提供的utils.sheet_to_string()方法将一张工作表转换成二维数组,方便操作处理。

但是如果单元格中包含日期数据,转换后拿到的将是一个数字。因为excel中对日期实际存储的是数字,通过格式化转换为日期。这就导致读取的日期也会被转成数字,无法与真正的数字区分。

解决方案

先说解决思路:区分单元格日期类型,遍历原始单元格对象,对日期类型单元格,读取格式化后的字符串。

  1. 将日期读取文件时就转为日期格式: XLSX.read(sheet, {cellDates: true})
  2. 遍历原始的单元格对象,根据对象中类型做区分,类型为 d,则是日期类型,需要读取格式化后的值,其他类型仍读取真实值(单元格对象的v值)。

这种形式导致无法直接使用utils.sheet_to_json()转换的二维数组数据,必须从原始的单元格对象中遍历,对日期类型做特别处理。

问题现象

工作表中的数字与日期经sheet.js解析后的单元格对象,类型是相同的。经utils.sheet_to_json()转换后会得到数字数组,而没有期望的日期。

import {utils} from 'xlsx';
// t 表示数据类型,v 表示实际值,w 表示格式化后展示的值
const sheet = {
  "A1": {t: "n", v: 44927, w: "2023年1月"},
  "A2": {t: "n", v: 44958, w: "2023年2月"}
  "B1": {t: "n", v: 100, w: "100"},
  "B2": {t: "n", v: 200, w: "200"},
}
// 转换工作表为二维数组:
cosnt arr = utils.sheet_to_json(sheet);
// arr 为:
// [
//   [44927, 100],
//   [44958, 200]
// ]

这就导致当存在日期数据时,转换后的二维数组无法区分日期与数字,导致无法直接使用。

解决过程

sheet.js提供了读取文件时,直接将日期转为日期格式的方式,需要在读取时,增加配置项{cellDates: true}:

import {read} from 'xlsx';
// ...
// 以antd中Form提交方法为例(也可以在Upload组件的beforeUpload中处理):
const onFinish = async (values) => {
  const file = values.files[0].originFileObj; // 得到原始文件对象
  if (file) {
    // 读取工作簿数据
    const bufferData = await file.arrayBuffer();
    const workbook = await read(bufferData, { cellDates: true });
    // 打印第一张工作表
    const firstSheetName = workbook.SheetNames[0];
    const firstSheet = workbook.Sheets[firstSheetName]
    console.log(firstSheet);
  }    
}

打印后会发现,日期格式已有专有的类型d,但是v值显示的js日期莫名少了3秒,utils.sheet_to_json()转换后会取v中的值,还是无法使用。

{t: 'd', v: Sat Dec 31 2022 23:59:17 GMT+0800 (中国标准时间), w: '2023年1月'}`

于是只能配合原始单元格对象、sheet_to_json()转换后的二维数组,一起过滤出真实的二维数组:遍历转换二维数组,得到对应的单元格位置,根据位置拿到原始单元格对象,将对象处理后使用。

核心示例代码:

// const sheet = ... // sheetjs解析的工作表对象
// const sheetArr = utils.sheet_to_json(sheet)
const records = [];
sheetArr.forEach((row, i) => {
  const record = []
  (row || []).forEach((val, j) => {
    // 根据二维数组行列位置,得到单元格编码,如 A2、B4
    const cellAddr = utils.encode_cell({r: i, c: j})
    // 根据单元格编码,得到单元格原始对象
    const cellObj = sheet[cellAddr];
    if (cellObj.t === 'd') {
      // 单元格为日期类型,取格式化后的字符串
      record.push(cellObj.w)
    } else {
      // 其他类型仍取实际值
      record.push(cellObj.v)
    }
  })
  records.push(record)
});
// records即为取得日期字符串的二维数组

其他

看文档配置中有类似日期的配置 dateNF,介绍不详,也没找到使用示例。自己尝试添加后,并没有效果。