1. 从零开始:为什么Node.js需要处理Excel文件?
如果你做过Web后端开发,或者写过一些需要处理数据的脚本,大概率会遇到一个场景:用户上传了一个Excel文件,你需要解析里面的数据;或者,你需要把数据库里的一堆数据,整理成一个格式规整的Excel报表,然后让用户下载。在Node.js生态里,处理Excel文件最常用、功能最全面的库之一就是xlsx。它几乎成了这个领域的“事实标准”,就像处理日期会用moment或dayjs一样。
这个库的强大之处在于,它完全用JavaScript实现,不依赖任何本地Excel程序(比如微软的Office或者金山的WPS),这意味着你可以在任何能运行Node.js的环境(比如Linux服务器、Docker容器)里使用它。无论是读取.xlsx、.xls还是.csv格式,还是生成包含复杂格式、公式、甚至多个工作表(Sheet)的Excel文件,它都能搞定。我最初接触它是因为一个数据迁移项目,需要把旧系统里导出的几十个Excel文件清洗、合并,再导入新数据库。手动操作是不可能的,用Python的pandas虽然也行,但整个技术栈是Node.js,为了一个功能引入另一种语言环境太折腾。xlsx库完美地解决了这个问题。
接下来的内容,我会以一个完整的、可运行的例子为主线,带你走一遍从安装、读取、处理到生成Excel文件的完整流程。我会重点讲清楚每个步骤背后的“为什么”,以及我在实际项目中踩过的那些坑。你会发现,用Node.js操作Excel,远比你想象的要简单和强大。
2. 环境准备与xlsx库的安装
在开始写代码之前,我们得先把“战场”布置好。这里假设你已经安装了Node.js和npm(Node包管理器)。如果你还没装,可以去Node.js官网下载LTS(长期支持)版本,安装过程基本是“下一步”到底。
注意:安装Node.js时,如果遇到系统提示禁止运行脚本的错误(比如热搜词里提到的“npm : 无法加载文件...因为在此系统上禁止运行脚本”),这是因为Windows系统的执行策略限制。解决方法是以管理员身份打开PowerShell,运行
Set-ExecutionPolicy RemoteSigned,然后选Y确认。这是个一次性操作。
创建一个新的项目目录,并初始化它:
mkdir node-excel-demo cd node-excel-demo npm init -y这行命令会生成一个package.json文件,记录项目的依赖。接下来,安装核心的xlsx库:
npm install xlsx安装完成后,你的package.json的dependencies里就会看到xlsx。这里有个小细节:xlsx库本身功能很全,但如果你需要处理非常大的文件(比如几百MB),可能会遇到内存问题。社区里也有像exceljs这样的库,它在处理大文件和流式写入方面有优势。但对于绝大多数日常场景——文件大小在几十MB以内——xlsx的稳定性和功能丰富度是首选。我选择它,就是因为其API稳定,社区活跃,遇到问题基本都能搜到解决方案。
为了测试,我们还需要一个Excel文件。你可以用WPS或Microsoft Excel自己创建一个简单的test.xlsx,包含一个“用户信息”表,内容如下:
| 姓名 | 年龄 | 城市 | 入职日期 |
|---|---|---|---|
| 张三 | 28 | 北京 | 2023-01-15 |
| 李四 | 35 | 上海 | 2020-08-22 |
| 王五 | 24 | 广州 | 2024-03-10 |
把它保存在项目根目录下。我们的第一个任务就是读取它。
3. 核心读取:把Excel文件变成JavaScript对象
读取是操作的第一步,目标是把磁盘上那个二进制的.xlsx文件,转换成我们在内存里可以随意操作的JavaScript数据结构。xlsx库提供了同步和异步两种读取方式,为了代码清晰,我们先从同步读法开始。
创建一个名为readExcel.js的文件:
const XLSX = require('xlsx'); const path = require('path'); // 1. 解析文件路径 const filePath = path.join(__dirname, 'test.xlsx'); // 2. 同步读取文件,得到工作簿(Workbook)对象 const workbook = XLSX.readFile(filePath); // 3. 查看工作簿里包含的所有工作表(Sheet)名称 console.log('工作表名称列表:', workbook.SheetNames); // 4. 根据名称获取第一个工作表 const firstSheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[firstSheetName]; // 5. 将工作表转换成JSON数据 // `header: 1` 选项表示将第一行作为标题行,并生成二维数组 // `header: ‘A’` 会生成键为单元格地址的对象。我们通常用`header: 1` const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); console.log('原始转换数据(二维数组):'); console.log(jsonData); // 6. 更常用的方式:将第一行作为对象的键 const dataWithHeaders = XLSX.utils.sheet_to_json(worksheet); console.log('\n结构化数据(对象数组):'); console.log(dataWithHeaders);运行这个脚本:node readExcel.js。你会看到类似下面的输出:
工作表名称列表: [ ‘Sheet1’ ] 原始转换数据(二维数组): [ [ ‘姓名’, ‘年龄’, ‘城市’, ‘入职日期’ ], [ ‘张三’, 28, ‘北京’, ‘2023-01-15’ ], [ ‘李四’, 35, ‘上海’, ‘2020-08-22’ ], [ ‘王五’, 24, ‘广州’, ‘2024-03-10’ ] ] 结构化数据(对象数组): [ { ‘姓名’: ‘张三’, ‘年龄’: 28, ‘城市’: ‘北京’, ‘入职日期’: ‘2023-01-15’ }, { ‘姓名’: ‘李四’, ‘年龄’: 35, ‘城市’: ‘上海’, ‘入职日期’: ‘2020-08-22’ }, { ‘姓名’: ‘王五’, ‘年龄’: 24, ‘城市’: ‘广州’, ‘入职日期’: ‘2024-03-10’ } ]核心原理与踩坑点:
workbook对象:这是XLSX.readFile返回的核心对象。它不仅仅包含数据,还包含所有工作表的定义、格式、公式等信息。workbook.SheetNames是一个数组,按顺序存放所有Sheet的名字。workbook.Sheets是一个对象,以Sheet名为键,对应的worksheet对象为值。worksheet对象:这是一个非常“原始”的数据结构,它本质上是一个以单元格地址(如A1,B2)为键,以单元格对象为值的映射。单元格对象包含v(原始值)、t(类型,如s字符串、n数字)、w(格式化文本)等属性。直接操作它很繁琐,所以我们需要XLSX.utils.sheet_to_json来转换。sheet_to_json的header选项:这是第一个容易踩坑的地方。header: 1:这是我们例子中的第一种用法。它告诉库,把工作表数据当成一个二维数组返回,第一行就是数组的第一个子数组。这种格式适合机器处理,但失去了列名的语义。header: ‘A’:返回一个对象数组,但每个对象的键是Excel的列字母(如{ A: ‘张三’, B: 28 }),很不直观,基本不用。- 不传或
header: ‘A’的另一种形式:这是我们例子中的第二种用法。库会默认将工作表的第一行作为JSON对象的键(属性名)。这是最常用、最直观的方式。但这里有个大坑:如果Excel第一行的某个单元格是空的,那么对应的属性名可能就是空字符串或者undefined,会导致后续数据处理出错。所以,确保你的Excel表头是完整、无合并单元格的。
- 数据类型识别:
xlsx会尽力识别单元格的数据类型。数字、日期会转换成JavaScript的Number和Date对象(但sheet_to_json默认输出时,日期会变成类似2023-01-15的ISO字符串)。布尔值会变成true/false。如果遇到它无法识别的格式,或者单元格里是公式,v值可能是原始公式字符串(如=A1+B1),你需要通过cell.f来判断。
异步读取:如果你的应用是Web服务器,在处理用户上传的文件时,应该使用异步API避免阻塞事件循环。xlsx库本身没有提供异步的readFile,但我们可以结合Node.js的fs.promises来实现:
const XLSX = require('xlsx'); const fs = require('fs').promises; async function readExcelAsync(filePath) { try { // 异步读取文件Buffer const fileBuffer = await fs.readFile(filePath); // 用Buffer进行解析 const workbook = XLSX.read(fileBuffer, { type: ‘buffer’ }); const worksheet = workbook.Sheets[workbook.SheetNames[0]]; return XLSX.utils.sheet_to_json(worksheet); } catch (error) { console.error(‘读取Excel文件失败:’, error); throw error; } }关键点在于XLSX.read的第二个参数{ type: ‘buffer’ },它告诉库我们提供的是一个二进制Buffer。
4. 数据处理:清洗、转换与计算
读取到JSON数据后,我们通常不会直接使用,而是要进行一系列清洗和转换。这是业务逻辑的核心部分。假设我们从dataWithHeaders这个对象数组开始。
场景一:数据清洗我们的数据里,“年龄”应该是数字,“入职日期”应该是日期对象。但读取进来可能都是字符串。
const cleanedData = dataWithHeaders.map(row => { // 创建一个新对象,避免修改原数据 const newRow = { …row }; // 清洗年龄:确保是数字 newRow.年龄 = Number(newRow.年龄); if (isNaN(newRow.年龄)) { newRow.年龄 = null; // 或者提供一个默认值 console.warn(`“${newRow.姓名}”的年龄“${row.年龄}”转换失败`); } // 清洗日期:将字符串转为Date对象 // 注意:Excel的日期在内部可能是一个数字(从1899-12-30开始的天数), // 但通过sheet_to_json转换后,如果单元格格式是日期,通常会得到ISO字符串。 // 这里我们假设读取到的是‘YYYY-MM-DD’字符串。 if (newRow.入职日期 && typeof newRow.入职日期 === ‘string’) { const dateObj = new Date(newRow.入职日期); if (isNaN(dateObj.getTime())) { // 检查日期是否无效 console.warn(`“${newRow.姓名}”的入职日期“${newRow.入职日期}”格式错误`); newRow.入职日期 = null; } else { newRow.入职日期 = dateObj; } } // 可以添加更多清洗逻辑,比如城市名称标准化 const cityMap = { ‘北京’: ‘Beijing’, ‘上海’: ‘Shanghai’, ‘广州’: ‘Guangzhou’ }; if (cityMap[newRow.城市]) { newRow.城市 = cityMap[newRow.城市]; } return newRow; }); console.log(‘\n清洗后的数据:’); console.log(cleanedData);场景二:数据计算与衍生字段基于现有数据,计算新的指标。例如,计算工龄(假设当前日期是2024-05-27)。
const currentDate = new Date(‘2024-05-27’); const dataWithSeniority = cleanedData.map(row => { if (row.入职日期 instanceof Date) { // 计算相差的毫秒数,转换为年(粗略计算) const diffTime = Math.abs(currentDate - row.入职日期); const diffYears = diffTime / (1000 * 60 * 60 * 24 * 365.25); row.工龄 = Math.floor(diffYears); // 取整 } else { row.工龄 = null; } return row; }); console.log(‘\n添加工龄后的数据:’); console.log(dataWithSeniority);场景三:数据筛选与聚合找出年龄大于30岁的员工,或者按城市分组统计平均年龄。
// 筛选 const olderThan30 = dataWithSeniority.filter(row => row.年龄 > 30); console.log(‘\n年龄大于30的员工:’, olderThan30); // 聚合 const cityStats = {}; dataWithSeniority.forEach(row => { if (!cityStats[row.城市]) { cityStats[row.城市] = { count: 0, totalAge: 0 }; } cityStats[row.城市].count += 1; cityStats[row.城市].totalAge += row.年龄; }); for (const city in cityStats) { cityStats[city].averageAge = (cityStats[city].totalAge / cityStats[city].count).toFixed(2); } console.log(‘\n按城市统计的平均年龄:’, cityStats);这些操作都是标准的JavaScript数组和对象处理,xlsx库的任务只是把数据从Excel里“搬”出来,剩下的就交给你的业务逻辑了。
5. 核心生成:从JSON数据到Excel文件
数据处理完了,接下来就是“写回去”或者生成一个新的Excel文件。这是xlsx库另一个强大的功能。我们想把dataWithSeniority这个包含了工龄的数据,写成一个新的Excel文件,并且希望它好看一点,比如把表头加粗,给“工龄”列加上颜色。
创建一个writeExcel.js文件:
const XLSX = require(‘xlsx’); const path = require(‘path’); // 假设这是我们处理好的数据 const dataToWrite = [ { ‘姓名’: ‘张三’, ‘年龄’: 28, ‘城市’: ‘Beijing’, ‘入职日期’: new Date(‘2023-01-15’), ‘工龄’: 1 }, { ‘姓名’: ‘李四’, ‘年龄’: 35, ‘城市’: ‘Shanghai’, ‘入职日期’: new Date(‘2020-08-22’), ‘工龄’: 4 }, { ‘姓名’: ‘王五’, ‘年龄’: 24, ‘城市’: ‘Guangzhou’, ‘入职日期’: new Date(‘2024-03-10’), ‘工龄’: 0 } ]; // 1. 创建一个新的工作簿 const workbook = XLSX.utils.book_new(); // 2. 将JSON数据转换为工作表对象 // 注意:Date对象会被转换,但格式是原始的。 const worksheet = XLSX.utils.json_to_sheet(dataToWrite); // 3. (可选但重要)定义单元格样式 // xlsx库的样式设置比较底层,需要通过`!cols`, `!rows`, `!merges`等属性操作。 // 我们先获取工作表的范围,知道它有多大。 const range = XLSX.utils.decode_range(worksheet[‘!ref’]); // ‘!ref’ 定义了工作表的数据范围,如 ‘A1:E4’ // 3.1 设置列宽 worksheet[‘!cols’] = [ { wch: 10 }, // 第1列(A列,姓名)宽度为10字符 { wch: 6 }, // 第2列(B列,年龄) { wch: 12 }, // 第3列(C列,城市) { wch: 12 }, // 第4列(D列,入职日期) { wch: 8 }, // 第5列(E列,工龄) ]; // 3.2 设置第一行(表头)的样式:加粗、居中 for (let C = range.s.c; C <= range.e.c; ++C) { // 遍历所有列 const cellAddress = XLSX.utils.encode_cell({ r: range.s.r, c: C }); // 第一行(r=0)的每个单元格地址 if (!worksheet[cellAddress]) continue; // 初始化单元格的样式对象 worksheet[cellAddress].s = { font: { bold: true }, alignment: { horizontal: ‘center’ } }; } // 3.3 为“工龄”列(E列)设置条件格式(示例:工龄>=3的标为浅绿色) // 这里演示的是直接设置单元格填充色,真正的条件格式更复杂,需要用到`xlsx`的扩展功能。 // 简单实现:遍历“工龄”列,手动判断并设置样式。 const工龄列索引 = 4; // E列是第5列,索引是4(从0开始) for (let R = range.s.r + 1; R <= range.e.r; ++R) { // 从第二行开始(跳过表头) const cellAddress = XLSX.utils.encode_cell({ r: R, c: 工龄列索引 }); const cell = worksheet[cellAddress]; if (cell && cell.v >= 3) { cell.s = { …(cell.s || {}), fill: { fgColor: { rgb: “FFC6EFCE” } } }; // 浅绿色 } } // 4. 将工作表添加到工作簿,并命名 XLSX.utils.book_append_sheet(workbook, worksheet, “员工信息表”); // 5. 写入文件 const outputPath = path.join(__dirname, ‘output_with_style.xlsx’); XLSX.writeFile(workbook, outputPath); console.log(`Excel文件已生成:${outputPath}`);运行node writeExcel.js,你会在目录下看到一个output_with_style.xlsx文件。用Excel或WPS打开它,你会发现表头加粗居中了,列宽合适了,而且李四的工龄是4年,他的“工龄”单元格被标上了浅绿色背景。
生成环节的深度解析与避坑指南:
json_to_sheet的局限性:这个函数非常方便,但它只处理数据。所有的样式(字体、颜色、边框、数字格式)都需要事后通过操作worksheet对象的单元格s属性来添加。s属性的结构遵循Excel的样式对象规范,学习起来有点成本。- 样式设置的复杂性:上面的例子只是设置了字体加粗、对齐和填充色。更复杂的样式,如边框、数字格式(如货币、百分比)、公式等,需要构造更复杂的
s对象。例如,将“入职日期”列设置为“YYYY年MM月DD日”格式:
Excel的数字格式代码是个单独的领域,需要查文档。// 假设入职日期在D列(索引3) for (let R = range.s.r + 1; R <= range.e.r; ++R) { const cellAddress = XLSX.utils.encode_cell({ r: R, c: 3 }); const cell = worksheet[cellAddress]; if (cell) { cell.z = ‘yyyy”年”mm”月”dd”日”;@’; // Excel的数字格式代码 // 注意:`z`是数字格式代码,`s`是样式对象。有时需要同时设置。 } }xlsx库的官方文档和源码中的/bits/90_ssf.js文件有一些例子。 - “!ref”的重要性:
worksheet[‘!ref’]定义了工作表的数据范围,比如A1:E4。在添加样式或遍历单元格时,一定要先解码这个范围(XLSX.utils.decode_range),否则可能会漏掉一些单元格,或者访问不存在的单元格导致错误。 - 写入性能:
XLSX.writeFile是同步的,对于大文件会阻塞。对于服务器应用,可以考虑使用XLSX.write生成Buffer,然后用fs的异步API写入,或者使用流(虽然xlsx对流支持不直接,但可以分片生成)。对于超大数据量(数十万行),可能需要考虑exceljs或node-xlsx-writer这类支持流式写入的库。
6. 高级应用:多工作表、公式与文件流处理
在实际项目中,需求往往更复杂。我们可能需要生成包含多个工作表的报表,或者在单元格里插入公式,甚至处理用户上传的Excel流。
6.1 创建包含多个工作表的工作簿
假设我们要生成一个报表,包含“数据总览”和“城市统计”两个Sheet。
const XLSX = require(‘xlsx’); // 数据总览Sheet的数据 const overviewData = [ /* … 员工数据 … */ ]; // 城市统计Sheet的数据(来自之前的聚合计算) const citySummaryData = Object.entries(cityStats).map(([city, stats]) => ({ ‘城市’: city, ‘员工数量’: stats.count, ‘平均年龄’: stats.averageAge })); const workbook = XLSX.utils.book_new(); // 创建第一个工作表 const overviewSheet = XLSX.utils.json_to_sheet(overviewData); XLSX.utils.book_append_sheet(workbook, overviewSheet, “数据总览”); // 创建第二个工作表 const summarySheet = XLSX.utils.json_to_sheet(citySummaryData); XLSX.utils.book_append_sheet(workbook, summarySheet, “城市统计”); // 甚至可以创建一个只包含图表链接或说明的Sheet const infoSheet = XLSX.utils.aoa_to_sheet([ // aoa_to_sheet: Array of Arrays 转工作表 [‘报表生成说明’], [‘生成时间:’, new Date().toLocaleString()], [‘数据来源:’, ‘内部数据库’], [], [‘注:城市统计表数据来源于数据总览表的聚合计算。’] ]); XLSX.utils.book_append_sheet(workbook, infoSheet, “说明”); XLSX.writeFile(workbook, ‘multi_sheet_report.xlsx’);6.2 在单元格中插入公式
Excel的公式是其灵魂。xlsx库支持写入公式,但不会计算公式的结果。公式的计算需要由Excel客户端(如Microsoft Excel, WPS)在打开文件时执行。
const worksheet = XLSX.utils.aoa_to_sheet([ [‘项目’, ‘预算’, ‘实际’, ‘差额’], [‘A项目’, 10000, 9500], [‘B项目’, 20000, 21000], [‘总计’, , , ] // 总计行,前两列空,差额列待计算 ]); // 在D2单元格(A项目差额)写入公式: =B2-C2 worksheet[‘D2’] = { f: ‘B2-C2’, t: ‘n’ }; // f 表示公式(forumula) // 在D3单元格(B项目差额)写入公式: =B3-C3 worksheet[‘D3’] = { f: ‘B3-C3’, t: ‘n’ }; // 在B4单元格(预算总计)写入公式: =SUM(B2:B3) worksheet[‘B4’] = { f: ‘SUM(B2:B3)’, t: ‘n’ }; // 在C4单元格(实际总计)写入公式: =SUM(C2:C3) worksheet[‘C4’] = { f: ‘SUM(C2:C3)’, t: ‘n’ }; // 在D4单元格(差额总计)写入公式: =SUM(D2:D3) 或者 =B4-C4 worksheet[‘D4’] = { f: ‘D2+D3’, t: ‘n’ }; // 注意:公式字符串需要符合Excel语法 const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, worksheet, “带公式”); XLSX.writeFile(wb, ‘with_formula.xlsx’);打开这个文件,你会看到单元格显示公式,点击单元格,编辑栏会显示=B2-C2这样的公式。只有当你在Excel/WPS里打开,或者手动触发计算后,才会显示计算结果。
6.3 处理文件上传流(Web应用场景)
在Express或Koa这样的Web框架中,用户通过表单上传Excel文件。我们通常使用multer这样的中间件来接收文件,它提供的是文件在服务器上的路径或Buffer。
const express = require(‘express’); const multer = require(‘multer’); const XLSX = require(‘xlsx’); const app = express(); const upload = multer({ dest: ‘uploads/’ }); // 文件暂存目录 app.post(‘/upload’, upload.single(‘excelFile’), (req, res) => { if (!req.file) { return res.status(400).json({ error: ‘请上传文件’ }); } try { // req.file.path 是multer保存的临时文件路径 const workbook = XLSX.readFile(req.file.path); const worksheet = workbook.Sheets[workbook.SheetNames[0]]; const data = XLSX.utils.sheet_to_json(worksheet); // … 处理你的数据逻辑 … // 处理完后,可以删除临时文件(可选) const fs = require(‘fs’); fs.unlink(req.file.path, (err) => { if (err) console.error(‘删除临时文件失败:’, err); }); res.json({ success: true, data: data /* 或处理后的结果 */ }); } catch (error) { console.error(‘处理上传文件时出错:’, error); res.status(500).json({ error: ‘文件处理失败’ }); } }); // 生成并提供Excel文件下载 app.get(‘/download’, (req, res) => { const data = [ /* … 从数据库或其他地方获取数据 … */ ]; const worksheet = XLSX.utils.json_to_sheet(data); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, “Sheet1”); // 生成二进制Buffer const excelBuffer = XLSX.write(workbook, { bookType: ‘xlsx’, type: ‘buffer’ }); // 设置HTTP响应头,告诉浏览器这是一个要下载的Excel文件 res.setHeader(‘Content-Disposition’, ‘attachment; filename=”report.xlsx”’); res.setHeader(‘Content-Type’, ‘application/vnd.openxmlformats-officedocument.spreadsheetml.sheet’); res.send(excelBuffer); // 发送Buffer }); app.listen(3000, () => console.log(‘服务器运行在端口3000’));这里的关键是:
- 上传:使用
XLSX.readFile读取服务器上的临时文件。 - 下载:使用
XLSX.write的{ type: ‘buffer’ }选项生成二进制Buffer,然后通过HTTP响应直接发送给前端。前端浏览器接收到这种响应头和二进制数据,就会触发文件下载。
7. 实战踩坑与性能优化经验谈
用了这么多年xlsx,坑没少踩。下面分享几个最常见的,以及对应的解决方案。
坑1:读取日期变成数字有时候,你读取一个日期单元格,得到的v值是一个像44927这样的数字。这是因为Excel内部用“序列日期”系统存储日期(从1899-12-30或1900-01-01开始计算的天数)。xlsx库的sheet_to_json默认会尝试转换,但如果单元格格式不是明确的日期格式,或者库没识别出来,就会返回原始数字。
解决方案:
- 使用
XLSX.utils.sheet_to_json(worksheet, { raw: false, dateNF: ‘YYYY-MM-DD’ })。raw: false会尝试让库输出格式化后的字符串(w属性),dateNF可以指定你想要的日期格式字符串。 - 手动转换:如果知道该列是日期,可以读取后手动转换。
xlsx提供了一个工具函数XLSX.SSF.parse_date_code(num),可以把那个数字转换成{ y, m, d, … }对象。const excelDateNum = 44927; const dateObj = XLSX.SSF.parse_date_code(excelDateNum); // dateObj 返回 { y: 2023, m: 1, d: 15, … } 注意m是从1开始的 const jsDate = new Date(dateObj.y, dateObj.m - 1, dateObj.d);
坑2:大文件内存溢出当你尝试用XLSX.readFile读取一个几百MB的Excel文件时,Node.js进程可能会因为内存不足而崩溃。
解决方案:
- 流式读取(部分支持):
xlsx库的XLSX.read函数可以接受一个type: ‘file’的选项,并配合cellStyles: false,cellDates: false等选项来减少内存占用,但它本质上还是会把文件内容读入内存。对于纯数据的大文件,可以尝试关闭样式解析。const workbook = XLSX.readFile(‘huge.xlsx’, { cellStyles: false, // 不解析样式 cellDates: false, // 不尝试转换日期 sheetStubs: false // 不保留空单元格存根 }); - 换库:如果文件真的巨大,考虑使用
exceljs,它支持流式读取(reader.stream)和流式写入(writer.stream),可以分块处理数据,对内存友好。 - 预处理:如果可能,让上游系统导出
.csv格式,用Node.js的fs.createReadStream和csv-parser等流式CSV解析器处理,这是处理海量数据最有效的方式。
坑3:合并单元格的处理如果你的Excel有合并单元格,sheet_to_json默认行为可能会让你丢失数据。例如,A1:A3合并了,值为“总计”,那么转换后只有第一行有这个值,A2和A3的位置可能是null或undefined。
解决方案:
- 读取合并单元格信息:
worksheet[‘!merges’]数组包含了所有合并单元格的范围。你需要自己写逻辑,在转换JSON时,将合并区域的值“填充”到每个对应的单元格位置。 - 使用
XLSX.utils.sheet_to_json(worksheet, { header: 1, raw: true })获取二维数组,然后自己遍历!merges来填充二维数组,最后再转换成你需要的对象格式。这个过程有点繁琐,但对于保持数据结构完整是必要的。
坑4:生成的文件用WPS打开提示“文件已损坏”或自动改后缀(对应热搜词:wps后台自动改xlsx到xlsm)。这个问题我遇到过好几次。通常不是因为xlsx库生成的文件真的坏了,而是因为文件里包含了一些WPS不兼容或谨慎对待的特性(比如宏、特定的公式函数、或某些样式的定义方式)。WPS出于安全考虑,可能会将其另存为.xlsm(启用宏的工作簿)或提示修复。
解决方案:
- 检查内容:确保你没有无意中写入了
xlsx不原生支持的复杂特性。最简单的测试方法是,用Microsoft Excel打开你生成的文件,看是否正常。 - 简化文件:如果只是需要数据,尝试生成时去掉所有样式(
s属性)、公式(f属性),只保留纯数据。 - 使用
bookType选项:XLSX.writeFile支持不同的bookType,如‘xlsx’,‘xlsm’,‘xlsb’,‘ods’等。如果你确定需要宏,可以显式指定bookType: ‘xlsm’。但xlsx库本身不支持创建VBA宏。 - 用户教育:在下载链接旁加个备注:“建议使用Microsoft Excel 2007以上版本或新版WPS打开”。
性能优化小技巧:
- 批量操作单元格样式:如果要对整行或整列设置样式,避免在循环里单个单元格设置
s属性。可以先构建一个样式对象,然后批量赋值。或者,对于大量相同样式的单元格,考虑使用“默认行/列样式”,但这在xlsx库中实现起来比较麻烦。 - 善用
sheet_to_json的选项:defval参数可以设置默认值,blankrows可以控制是否跳过空行。合理使用它们可以简化后续的数据清洗逻辑。 - 对于只读场景:如果只需要读取特定列或特定区域的数据,可以使用
XLSX.utils.sheet_to_json(worksheet, { range: ‘A1:C10’ })来限定范围,减少不必要的数据解析。
最后,xlsx库的文档(在GitHub仓库的README和docbits/目录下)是宝库,里面有很多高级用例和API说明。遇到奇怪的问题,先去翻翻源码和文档,往往能找到答案。