Node.js 大战EXCEL电子表格读写

1,067 阅读7分钟

Node.js需要借助第三方模块来读写EXCEL。
可以备选的模块有 Sheetjs(Node.js中模块名为xlsx)、exceljs、node-xlsx等。
这里选用xlsx进行EXCLE文件读写
官网docs.sheetjs.com
明显缺陷:默认的是社区版本(CE),无法保持EXCEL原有样式

演示环境:

1.Node.js v21.0.0、npm 10.2.0(以下代码支持Node.js v18.x以上)
2.系统Win11+MS Office2021。文章编辑Markdown主题awesome-green
3.[可选]VSCode查看EXCEL插件:Office Viewer

〇、学一点EXCLE基础知识

三个重要概念(由大到小) 工作簿→工作表→单元格
工作簿(Workbook):整个EXCEL文件,可包含一到多个工作表。
工作表(Worksheet):一个由列行构成的数据表格。
单元格(Cell):工作表中最小的组成单位。通常由列和行组成,
例如:
G5表示G列第5行的单元格。

图片.png width="50%

一、读取EXCEL文件

1. Node.js安装xlsx模块

npm -v  //查看 Node.js模块管理工具npm 版本,测试环境正常
npm install xlsx  // npm install 模块名

2.读取EXCEL文件(一个工作簿 )

图片.png

const XLSX = require('xlsx')
const 工作簿  = XLSX.readFile('百万.xlsx')
console.log( 工作簿 )

读取到的 工作簿工作簿.SheetNames工作簿.Sheets属性,内容如下:

//工作簿
{
  /*   ...此处隐藏若干属性...  */
  SheetNames: [...],   //所有工作表名
  Sheets: {...},       //所有工作表数据
  /*   ...此处隐藏若干属性...  */
}

//工作簿.SheetNames 所有工作表名  Array类型
[ '第一张工作表', '第二张工作表' ]
//工作簿.Sheets  所有工作表数据  Object类型
{
  '第一张工作表': {
    '!ref': 'A1:B3',
    A1: {
      t: 's', //数据类型 s 表示字符串
      v: '序号',
      r: '<t>序号</t><phoneticPr fontId="1" type="noConversion"/>',
      h: '序号',
      w: '序号'
    },
    B1: {
      t: 's',
      v: '姓名',
      r: '<t>姓名</t><phoneticPr fontId="1" type="noConversion"/>',
      h: '姓名',
      w: '姓名'
    },
    A2: { t: 'n', v: 1, w: '1' },
    B2: {
      t: 's',
      v: '百万',
      r: '<t>百万</t><phoneticPr fontId="1" type="noConversion"/>',
      h: '百万',
      w: '百万'
    },
    A3: { t: 'n', v: 2, w: '2' },
    B3: {
      t: 's',
      v: '牛牛',
      r: '<t>牛牛</t><phoneticPr fontId="1" type="noConversion"/>',
      h: '牛牛',
      w: '牛牛'
    },
    '!margins': {
      left: 0.7,
      right: 0.7,
      top: 0.75,
      bottom: 0.75,
      header: 0.3,
      footer: 0.3
    }
  },
  '第二张工作表': {
    '!margins': {
      left: 0.7,
      right: 0.7,
      top: 0.75,
      bottom: 0.75,
      header: 0.3,
      footer: 0.3
    }
  }
}

2.1 工作表中 直接获取指定单元格的值

console.table(工作簿.Sheets) 打印的值如下 图片.png 在工作表数据中,属性直接就是单元格的坐标。

// 截取的A1单元格的值
{
    '第一张工作表': {
      A1: {
        t: 's', //数据类型 s 表示字符串
        v: '序号',
        r: '<t>序号</t><phoneticPr fontId="1" type="noConversion"/>',
        h: '序号',
        w: '序号'
      }
    }
}

// 1.获取第一个工作表的A1单元格的值
工作簿.Sheets['第一张工作表'].A1.v  // 序号

// 2.获取第一个工作表的A1单元格的值
let 表名 = 工作簿.SheetNames[0]    // 获取第一张工作表的名字
工作簿.Sheets[表名].A1.v   // 序号

2.2 [推荐]工作表中 处理后 获取单元格的值

使用XLSX.utils.sheet_to_json( 工作表 ),将工作表数据转成JSON格式

const XLSX = require('xlsx')
const 工作簿  = XLSX.readFile('百万.xlsx')
let name  =  工作簿.SheetNames[0]   
let 工作表  = 工作簿.Sheets[name]
            //sheet_to_json 将工作表数据转成JSON格式
let jsonArr = XLSX.utils.sheet_to_json( 工作表  )

console.table(jsonArr)
console.log(jsonArr)
//console.table(jsonArr)
┌─────────┬──────┬────────┐
│ (index) │ 序号 │  姓名  │
├─────────┼──────┼────────┤
│    01'百万' │
│    12'牛牛' │
└─────────┴──────┴────────┘
//console.log(jsonArr)
[ 
  { '序号': 1, '姓名': '百万' }, 
  { '序号': 2, '姓名': '牛牛' } 
]

二、写入EXCEL文件

1.获得一个空的 EXCEL文件

使用 xlsx 库来创建一个新的工作簿。并在其中添加一个工作表,然后将该工作簿写入一个名为 "文件名.xlsx" 的 Excel 文件

const XLSX = require('xlsx')
const 工作簿 = XLSX.utils.book_new( )  //创建一个新的工作簿
工作簿.SheetNames[0] = '第一张表名' //工作簿至少要有一个工作表
XLSX.writeFile(工作簿,'文件名.xlsx' )//输出到 指定文件名的EXCEL文件,注意必须要有.xlsx

2.创建有多张表的 EXCEL文件

GPQWF06@4[Y{74%])YGWP~D.png 采用了 XLSX.utils.aoa_to_sheet(arr) 将数组转化成表格数据
XLSX.utils.book_append_sheet(工作簿,工作表,'表名你的名字')向工作簿追加新表

const XLSX = require('xlsx')
// 第一步 创建一个新的工作簿  
const 工作簿 = XLSX.utils.book_new() 

// 第二步 创建数据,写入工作表
let arr = [
    ['序号','姓名'],
    [1 ,'你的名字']
]
            //aoa_to_sheet 将二维数组转换成 电子表格(sheet)
let 工作表  = XLSX.utils.aoa_to_sheet(arr)  

// 第三步 把工作表添加到工作簿中 
XLSX.utils.book_append_sheet(工作簿,工作表,'表名你的名字')  //第一个表
XLSX.utils.book_append_sheet(工作簿,工作表,'表名室友名字')  //第二个表

// 第四步 生成EXCEL文件
XLSX.writeFile(工作簿,'EXCEL文件名.xlsx' )

三、完整代码附录

1.XLSX对象

{
  version: '0.18.5',
  parse_xlscfb: [Function: parse_xlscfb],
  parse_zip: [Function: parse_zip],
  read: [Function: readSync],
  readFile: [Function: readFileSync],
  readFileSync: [Function: readFileSync],
  write: [Function: writeSync],
  writeFile: [Function: writeFileSync],
  writeFileSync: [Function: writeFileSync],
  writeFileAsync: [Function: writeFileAsync],
  utils: {
    encode_col: [Function: encode_col],
    encode_row: [Function: encode_row],
    encode_cell: [Function: encode_cell],
    encode_range: [Function: encode_range],
    decode_col: [Function: decode_col],
    decode_row: [Function: decode_row],
    split_cell: [Function: split_cell],
    decode_cell: [Function: decode_cell],
    decode_range: [Function: decode_range],
    format_cell: [Function: format_cell],
    sheet_add_aoa: [Function: sheet_add_aoa],
    sheet_add_json: [Function: sheet_add_json],
    sheet_add_dom: [Function: sheet_add_dom],
    aoa_to_sheet: [Function: aoa_to_sheet],
    json_to_sheet: [Function: json_to_sheet],
    table_to_sheet: [Function: parse_dom_table],
    table_to_book: [Function: table_to_book],
    sheet_to_csv: [Function: sheet_to_csv],
    sheet_to_txt: [Function: sheet_to_txt],
    sheet_to_json: [Function: sheet_to_json],
    sheet_to_html: [Function: sheet_to_html],
    sheet_to_formulae: [Function: sheet_to_formulae],
    sheet_to_row_object_array: [Function: sheet_to_json],
    sheet_get_cell: [Function: ws_get_cell_stub],
    book_new: [Function: book_new],
    book_append_sheet: [Function: book_append_sheet],
    book_set_sheet_visibility: [Function: book_set_sheet_visibility],
    cell_set_number_format: [Function: cell_set_number_format],
    cell_set_hyperlink: [Function: cell_set_hyperlink],
    cell_set_internal_link: [Function: cell_set_internal_link],
    cell_add_comment: [Function: cell_add_comment],
    sheet_set_array_formula: [Function: sheet_set_array_formula],
    consts: { SHEET_VISIBLE: 0, SHEET_HIDDEN: 1, SHEET_VERY_HIDDEN: 2 }
  },
  writeXLSX: [Function: writeSyncXLSX],
  writeFileXLSX: [Function: writeFileSyncXLSX],
  SSF: {
    format: [Function: SSF_format],
    load: [Function: SSF_load],
    _table: {
      '0': 'General',
      '1': '0',
      '2': '0.00',
      '3': '#,##0',
      '4': '#,##0.00',
      '9': '0%',
      '10': '0.00%',
      '11': '0.00E+00',
      '12': '# ?/?',
      '13': '# ??/??',
      '14': 'm/d/yy',
      '15': 'd-mmm-yy',
      '16': 'd-mmm',
      '17': 'mmm-yy',
      '18': 'h:mm AM/PM',
      '19': 'h:mm:ss AM/PM',
      '20': 'h:mm',
      '21': 'h:mm:ss',
      '22': 'm/d/yy h:mm',
      '37': '#,##0 ;(#,##0)',
      '38': '#,##0 ;[Red](#,##0)',
      '39': '#,##0.00;(#,##0.00)',
      '40': '#,##0.00;[Red](#,##0.00)',
      '45': 'mm:ss',
      '46': '[h]:mm:ss',
      '47': 'mmss.0',
      '48': '##0.0E+0',
      '49': '@',
      '56': '"上午/下午 "hh"時"mm"分"ss"秒 "'
    },
    load_table: [Function: SSF_load_table],
    parse_date_code: [Function: SSF_parse_date_code],
    is_date: [Function: fmt_is_date],
    get_table: [Function: get_table]
  },
  stream: {
    to_json: [Function: write_json_stream],
    to_html: [Function: write_html_stream],
    to_csv: [Function: write_csv_stream],
    set_readable: [Function: set_readable]
  },
  CFB: {
    version: '1.2.1',
    find: [Function: find],
    read: [Function: read],
    parse: [Function: parse],
    write: [Function: write],
    writeFile: [Function: write_file],
    utils: {
      cfb_new: [Function: cfb_new],
      cfb_add: [Function: cfb_add],
      cfb_del: [Function: cfb_del],
      cfb_mov: [Function: cfb_mov],
      cfb_gc: [Function: cfb_gc],
      ReadShift: [Function: ReadShift],
      CheckField: [Function: CheckField],
      prep_blob: [Function: prep_blob],
      bconcat: [Function (anonymous)],
      use_zlib: [Function: use_zlib],
      _deflateRaw: [Function: _deflate],
      _inflateRaw: [Function: _inflate],
      consts: [Object]
    }
  }
}

2.工作簿对象

{
  Directory: {
    workbooks: [ '/xl/workbook.xml' ],
    sheets: [ '/xl/worksheets/sheet1.xml', '/xl/worksheets/sheet2.xml' ],
    charts: [],
    dialogs: [],
    macros: [],
    rels: [],
    strs: [],
    comments: [],
    threadedcomments: [],
    links: [],
    coreprops: [ '/docProps/core.xml' ],
    extprops: [ '/docProps/app.xml' ],
    custprops: [],
    themes: [ '/xl/theme/theme1.xml' ],
    styles: [ '/xl/styles.xml' ],
    vba: [],
    drawings: [],
    metadata: [ '/xl/metadata.xml' ],
    people: [],
    TODO: [],
    xmlns: 'http://schemas.openxmlformats.org/package/2006/content-types',
    calcchain: '',
    sst: '',
    style: '/xl/styles.xml',
    defaults: {
      xml: 'application/xml',
      bin: 'application/vnd.ms-excel.sheet.binary.macroEnabled.main',
      vml: 'application/vnd.openxmlformats-officedocument.vmlDrawing',
      data: 'application/vnd.openxmlformats-officedocument.model+data',
      bmp: 'image/bmp',
      png: 'image/png',
      gif: 'image/gif',
      emf: 'image/x-emf',
      wmf: 'image/x-wmf',
      jpg: 'image/jpeg',
      jpeg: 'image/jpeg',
      tif: 'image/tiff',
      tiff: 'image/tiff',
      pdf: 'application/pdf',
      rels: 'application/vnd.openxmlformats-package.relationships+xml'
    }
  },
  Workbook: {
    AppVersion: {},
    WBProps: {
      CodeName: 'ThisWorkbook',
      allowRefreshQuery: false,
      autoCompressPictures: true,
      backupFile: false,
      checkCompatibility: false,
      date1904: false,
      defaultThemeVersion: 0,
      filterPrivacy: false,
      hidePivotFieldList: false,
      promptedSolutions: false,
      publishItems: false,
      refreshAllConnections: false,
      saveExternalLinkValues: true,
      showBorderUnselectedTables: true,
      showInkAnnotation: true,
      showObjects: 'all',
      showPivotChartFilter: false,
      updateLinks: 'userSet'
    },
    WBView: [],
    Sheets: [ [Object], [Object] ],
    CalcPr: {
      calcCompleted: 'true',
      calcMode: 'auto',
      calcOnSave: 'true',
      concurrentCalc: 'true',
      fullCalcOnLoad: 'false',
      fullPrecision: 'true',
      iterate: 'false',
      iterateCount: '100',
      iterateDelta: '0.001',
      refMode: 'A1'
    },
    Names: [],
    xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
    Views: [ {} ]
  },
  Props: {
    Application: 'SheetJS',
    HyperlinksChanged: false,
    SharedDoc: false,
    LinksUpToDate: false,
    ScaleCrop: false,
    Worksheets: 2,
    SheetNames: [ '表名你的名字', '表名室友名字' ]
  },
  Custprops: {},
  Deps: {},
  Sheets: {
    '表名你的名字': {
      '!ref': 'A1:B2',
      A1: [Object],
      B1: [Object],
      A2: [Object],
      B2: [Object]
    },
    '表名室友名字': {
      '!ref': 'A1:B2',
      A1: [Object],
      B1: [Object],
      A2: [Object],
      B2: [Object]
    }
  },
  SheetNames: [ '表名你的名字', '表名室友名字' ],
  Strings: [],
  Styles: {
    NumberFmt: [
      'General',
      '0',
      '0.00',
      '#,##0',
      '#,##0.00',
      <4 empty items>,
      '0%',
      '0.00%',
      '0.00E+00',
      '# ?/?',
      '# ??/??',
      'm/d/yy',
      'd-mmm-yy',
      'd-mmm',
      'mmm-yy',
      'h:mm AM/PM',
      'h:mm:ss AM/PM',
      'h:mm',
      'h:mm:ss',
      'm/d/yy h:mm',
      <14 empty items>,
      '#,##0 ;(#,##0)',
      '#,##0 ;[Red](#,##0)',
      '#,##0.00;(#,##0.00)',
      '#,##0.00;[Red](#,##0.00)',
      <4 empty items>,
      'mm:ss',
      '[h]:mm:ss',
      'mmss.0',
      '##0.0E+0',
      '@',
      <6 empty items>,
      '"上午/下午 "hh"時"mm"分"ss"秒 "'
    ],
    Fonts: [ [Object] ],
    Fills: [ [Object], [Object] ],
    Borders: [ {} ],
    CellXf: [ [Object] ]
  },
  Themes: {},
  SSF: {
    '0': 'General',
    '1': '0',
    '2': '0.00',
    '3': '#,##0',
    '4': '#,##0.00',
    '9': '0%',
    '10': '0.00%',
    '11': '0.00E+00',
    '12': '# ?/?',
    '13': '# ??/??',
    '14': 'm/d/yy',
    '15': 'd-mmm-yy',
    '16': 'd-mmm',
    '17': 'mmm-yy',
    '18': 'h:mm AM/PM',
    '19': 'h:mm:ss AM/PM',
    '20': 'h:mm',
    '21': 'h:mm:ss',
    '22': 'm/d/yy h:mm',
    '37': '#,##0 ;(#,##0)',
    '38': '#,##0 ;[Red](#,##0)',
    '39': '#,##0.00;(#,##0.00)',
    '40': '#,##0.00;[Red](#,##0.00)',
    '45': 'mm:ss',
    '46': '[h]:mm:ss',
    '47': 'mmss.0',
    '48': '##0.0E+0',
    '49': '@',
    '56': '"上午/下午 "hh"時"mm"分"ss"秒 "'
  }
}