如果你的工作里也有一项雷打不动的周报/月报任务:把各个门店、各个小组、各个渠道发来的Excel文件一个个打开,把数据从第2行复制到一张汇总表里,遇到文件多一些(比如二十几、三十几份)就复制到手酸,还总要提心吊胆有没有漏行——那你一定会喜欢这篇文章。
这篇文章要分享的是用WPS JS宏批量合并Excel文件的完整实战方案,3步实现跨工作簿数据汇总,并附上可直接改改就用的完整代码。WPS JS宏是WPS表格内置的JavaScript脚本宏,不需要额外安装插件,也不需要啃复杂的VBA语法。只要你写过一点点JS,哪怕只是前端搬砖的水平,也能很快上手。我下面会把思路、关键API、步骤、避坑点全部拆开讲一遍,保证你看完能直接落地到自己每月都躲不掉的表格合并任务里。
1. 整体思路:为什么选JS宏,合并逻辑怎么设计
1.1 为什么是WPS JS宏,而不是VBA或手动复制
先说个大实话:手动复制粘贴是最容易出错的方式,尤其当你有二三十个文件时,漏一行、错一列、多复制一个空行都是家常便饭。我见过有人把三个月的数据按周合并,结果某个月份的文件名写错,导致整段数据缺失,最后复盘时才发现。手动方案唯一的好处是“不用学东西”,但代价是你的时间、精力和数据质量。
那为什么不用VBA?WPS对VBA的支持其实一直有点微妙:部分版本需要单独安装VBA组件,装了之后不同电脑的兼容性还可能不一致。而JS宏是WPS原生内置的脚本宏,从开发工具面板直接就能打开编辑器,语法是JavaScript,对很多写过脚本的人来说几乎没有学习曲线。更重要的是,JS宏和WPS的集成度确实高,日常的Workbooks、Sheets、Range、Cells这套对象模型都能用,足以覆盖“批量打开-读数据-写入汇总表”的完整流程。
我的经验是:如果只是“合并数据”这个需求,JS宏比VBA更友好。你不需要理解复杂的类模块,不需要关心参数传递方式,只需要按顺序调用几个对象方法,把数据读出来、写进去,就算跑通了。哪怕是没接触过宏的人,照着下面的代码抄一遍,也能在两分钟内运行起来。
1.2 合并逻辑的分层设计
很多新手一开始想得很简单:写个循环,打开工作簿,选中数据区域,复制,粘贴到汇总表,关掉,下一个。这个思路本身没错,但真正跑起来会踩一堆坑:有的文件里不止一个Sheet、有的表头是两行、有的数据区域里有空行、有的文件本身就没数据、有的列顺序还不一样。
所以我建议把合并拆成三个独立环节来写代码:
- 文件获取层:扫描指定文件夹,找出所有符合条件的Excel文件,得到一个文件路径列表。
- 数据读取层:逐个打开工作簿,定位第一个Sheet的有效数据区域,把数据统一读取成二维数组。
- 数据写入层:把二维数组一次性写入汇总表,按顺序向下追加。
这个分层的好处是:任何一个环节出问题,都能单独排查、单独测试。比如文件很多时,你可以先只扫描不读取,把文件清单打出来检查一遍;或者先用两个测试文件只做读取,确认数据格式没问题,再真正写入。比起一上来就写一个几百行的“一气呵成”脚本,这种搭乐高式的拆解更稳。
顺便说一句,合并时最怕的不是脚本不会写,而是源文件“不听话”。所以在设计逻辑时,一定要留一个“来源文件”列的位置,把每一行数据从哪个文件来的记录下来。这不是多余,这是救命。后面我会在代码里专门加上这个功能。
2. 关键API与核心细节:把WPS JS宏的脾气摸清楚
2.1 核心对象模型速览
WPS JS宏的操作方式和VBA非常像,都是通过Application → Workbooks → Workbook → Sheets → Sheet → Range/Cells这套对象模型来操作表格。你不需要背全部API,只需要记住这几个今天要用的:
Workbooks.Open(文件路径):打开一个工作簿,返回Workbook对象。wb.Sheets.Item(索引):获取第几张工作表,索引从1开始。sheet.UsedRange:获取工作表已使用的区域,返回一个Range对象。sheet.Cells(行号, 列号):定位到某个单元格。range.Value2:读取或设置区域的值,返回的是数组或标量。wb.Close(false):关闭工作簿,参数false表示不保存修改。
有一个细节必须提醒:JScript对大小写敏感。Workbooks.Open不能写成workbooks.open,ActiveWorkbook不能写成activeWorkbook。如果你之前写VBA写习惯了,刚一上手JS宏很容易在这里被卡住。另外,JS宏里的属性名和VBA基本保持一致,比如Rows.Count、Columns.Count,这算是一个福利。
还有一个容易被忽略的点:UsedRange并不一定从A1开始。如果某个Sheet里只有C5:F10有数据,那么UsedRange.Row返回5,UsedRange.Column返回3。如果代码里写死了从第1行第1列开始读,就会读错区域。所以后面完整代码里,我会用UsedRange.Row和UsedRange.Column动态获取起始位置,而不是直接写死“A1”。
2.2 文件遍历:用FileSystemObject扫描文件夹
批量合并,首先得解决“怎么拿到一个文件夹下的所有Excel文件路径”。这里我用的是Windows系统自带的Scripting.FileSystemObject组件,通过ActiveXObject在WPS JS宏里调用。基本用法是:
var fso = new ActiveXObject("Scripting.FileSystemObject"); var folder = fso.GetFolder(folderPath); var fileEnum = new Enumerator(folder.Files); for (; !fileEnum.atEnd(); fileEnum.moveNext()) { var f = fileEnum.item(); // f.Path就是完整文件路径 }这里有几个兼容性问题需要注意。在某些安全策略比较严格的电脑上,ActiveXObject会被禁用,代码会直接报错。遇到这种情况,最快的替代方案就是改用手动文件列表:在代码顶部维护一个数组,把要合并的文件完整路径一个个填进去。虽然麻烦一点,但绝对能用。我在后面的完整代码里会把这两种方式都写出来,你按自己的环境选。
另外,Enumerator这个对象也是从JScript里继承过来的,遍历文件集合很像是早期IE时代写脚本的感觉。如果你以前没见过,别慌,它就是一段固定写法,照着用即可。
2.3 读写性能与常见陷阱
批量合并的核心性能问题只有一个:不要逐格读写单元格。
用生活类比你就能理解了:逐格复制就像搬家时一次搬一本书,来回跑几十趟;而一次性读取整个数据区域再一次性写入,相当于找了辆小推车,一趟拉走。在几十个文件、每个文件几百行的场景下,前者慢到你怀疑电脑死机,后者基本是秒级完成。
所以代码里必须用“区域到数组,数组到区域”的方式:
var data = sourceRange.Value2; // 一次读成数组 targetRange.Value2 = data; // 一次写入区域这样做的另一个好处是减少WPS的界面刷新频率。有些教程会让你关掉屏幕刷新,比如Application.ScreenUpdating = false,但WPS JS宏对这个属性的支持在部分版本里不稳定,所以我不把它当主方案。只要你不是逐格读写,界面刷新的问题其实影响很小。
还有一个坑:Value2返回的数据结构在不同场景下不一样。单个单元格返回标量,单行或单列返回一维数组,多行多列返回二维数组(数组的数组)。如果你的源数据刚好只有一行,或者只有一列,直接用Value2拿到的可能不是你想当然的“二维数组”,写入时就会出现行列错位。这个情况我会在完整代码里做一个统一转换函数,把各种情况都规整成二维数组。
3. 实战:3步完成批量合并并附完整代码
3.1 第1步:准备环境
打开WPS表格,新建一个空白工作簿,这个工作簿就是将来的汇总表。接着点击菜单栏的“开发工具”,找到“JS宏编辑器”按钮,点击进入编辑器界面。如果你没看到“开发工具”选项卡,大概率是WPS把宏功能藏起来了,去“文件→选项→信任中心”里把“启用宏”相关的选项打开,或者查看当前WPS版本是否支持JS宏。
进入JS宏编辑器后,在左侧工程区找到当前工作簿对应的模块,新建一个模块,把下面的代码整体粘贴进去。粘贴完之后,选中主函数名MergeAllExcel,点击工具栏上的运行按钮,脚本就会开始执行。
需要注意,WPS JS宏的编辑器界面和VBA编辑器不一样,它更像一个轻量级的代码编辑器。你不需要保存什么特殊格式,代码写完直接留在编辑区即可。运行前建议先把汇总工作簿存一次盘,避免意外退出导致白忙活。
3.2 第2步:配置参数
运行前,只需要修改代码开头的四个配置项,别的不需要动:
| 参数 | 含义 | 默认值 | 说明 |
|---|---|---|---|
folderPath | 待合并文件所在文件夹路径 | "D:\\待合并数据\\" | 末尾要有双反斜杠 |
headerRows | 每个文件表头占几行 | 1 | 数据从表头下一行开始合并 |
addSourceColumn | 是否添加“来源文件”列 | true | 强烈建议保留 |
manualFiles | 手动文件列表开关 | false | 无法使用FileSystemObject时开启 |
folderPath是核心参数,改成你自己的文件夹路径就可以。注意Windows路径里的反斜杠要写成双反斜杠,比如"D:\\我的数据\\2025年1月\\",否则JS会把\我当成转义字符,导致路径解析错误,这也是一个非常常见的小坑。
headerRows默认1,表示每个源文件的第一行是表头,数据从第二行开始读。如果某些文件的表头占了两行,比如上面一行是“XX公司月度数据”,下面一行才是真正的列名,那这里就要填2。不管填几,最终生成汇总表时,只保留第一个文件的表头,后续文件的数据都从表头下面开始追加。
addSourceColumn默认true。开启后汇总表最左边会多一列“来源文件”,每个数据行后面记录它来自哪个文件。这个字段在做数据核对、分析回溯时非常有用,我建议你永远不要关掉它。
3.3 第3步:运行并验收
在JS宏编辑器里选中MergeAllExcel,点运行。脚本会按顺序做这些事情:扫描文件夹→逐个打开工作簿→读取第一个Sheet的有效数据区域→跳过表头→把数据追加到汇总表→关闭工作簿→全部完成后弹窗提示。
运行结束后,回到WPS表格界面,检查汇总表是否符合预期。主要看三点:第一,表头是否正确且只有一份;第二,数据总行数是否等于各文件数据行数之和;第三,“来源文件”列是否能和每个数据行对应上。如果某个文件的数据没进来,脚本会在弹窗中提示是哪个文件出了问题。
3.4 完整代码
// ========== 批量合并Excel文件:主入口 ========== // 使用方式:在WPS表格中打开“开发工具→JS宏编辑器”, // 新建模块后整体粘贴本代码,选中 MergeAllExcel 函数运行。 function MergeAllExcel() { // ===== 配置区开始 ===== var folderPath = "D:\\待合并数据\\"; // 改成你的文件夹路径,注意双反斜杠 var headerRows = 1; // 源文件表头行数,默认1 var addSourceColumn = true; // 是否在汇总表中添加“来源文件”列 var manualFiles = false; // true=使用下方手动文件列表,false=自动扫描文件夹 var manualFileList = [ // 如果 manualFiles 为 true,把要合并的文件路径都填在这里 // "D:\\待合并数据\\门店A.xlsx", // "D:\\待合并数据\\门店B.xlsx", ]; // ===== 配置区结束 ===== // 获取文件列表 var files = []; if (manualFiles) { files = manualFileList; } else { files = GetExcelFiles(folderPath); } if (files.length == 0) { MsgBox("没有找到任何Excel文件,请检查文件夹路径或手动文件列表。"); return; } // 汇总表使用当前活动工作簿的第一个Sheet var targetSheet = ActiveWorkbook.Sheets.Item(1); targetSheet.Cells.ClearContents(); var destRow = 1; // 当前写入的目标行号 var firstFile = true; // 是不是第一个文件,第一个文件写表头 for (var i = 0; i < files.length; i++) { var filePath = files[i]; try { // 第1步:打开工作簿,取第一个Sheet var wb = Workbooks.Open(filePath); var srcSheet = wb.Sheets.Item(1); // 动态获取已使用区域的起始行列和行列数 var used = srcSheet.UsedRange; var firstRow = used.Row; // 已使用区域起始行 var firstCol = used.Column; // 已使用区域起始列 var totalRows = used.Rows.Count; // 已使用区域总行数 var totalCols = used.Columns.Count; // 已使用区域总列数 // 如果去掉表头后没有数据,跳过这个文件 if (totalRows - headerRows <= 0) { wb.Close(false); continue; } // 第2步:读取数据 var dataArr = []; var actualCols = totalCols; if (firstFile) { // 第一个文件:读取表头+数据,整体写入 var headerRange = srcSheet.Range( srcSheet.Cells(firstRow, firstCol), srcSheet.Cells(firstRow + headerRows - 1, firstCol + totalCols - 1) ); var headerData = headerRange.Value2; headerData = normalizeTo2DArray(headerData, headerRows, totalCols); var dataRange = srcSheet.Range( srcSheet.Cells(firstRow + headerRows, firstCol), srcSheet.Cells(firstRow + totalRows - 1, firstCol + totalCols - 1) ); dataArr = dataRange.Value2; dataArr = normalizeTo2DArray(dataArr, totalRows - headerRows, totalCols); // 因为有来源列,先把列数扩展一列 actualCols = totalCols + (addSourceColumn ? 1 : 0); // 写入表头 if (addSourceColumn) { targetSheet.Cells(destRow, 1).Value2 = "来源文件"; } var headerWriteStartCol = addSourceColumn ? 2 : 1; targetSheet.Range( targetSheet.Cells(destRow, headerWriteStartCol), targetSheet.Cells(destRow + headerRows - 1, headerWriteStartCol + totalCols - 1) ).Value2 = headerData; destRow += headerRows; // 写入第一个文件的数据 var firstDataStartCol = addSourceColumn ? 2 : 1; targetSheet.Range( targetSheet.Cells(destRow, firstDataStartCol), targetSheet.Cells(destRow + dataArr.length - 1, firstDataStartCol + totalCols - 1) ).Value2 = dataArr; // 写来源文件列 if (addSourceColumn) { for (var r = 0; r < dataArr.length; r++) { targetSheet.Cells(destRow + r, 1).Value2 = filePath; } } destRow += dataArr.length; firstFile = false; } else { // 后续文件:跳过表头,只读数据 var nextDataRange = srcSheet.Range( srcSheet.Cells(firstRow + headerRows, firstCol), srcSheet.Cells(firstRow + totalRows - 1, firstCol + totalCols - 1) ); dataArr = nextDataRange.Value2; dataArr = normalizeTo2DArray(dataArr, totalRows - headerRows, totalCols); var nextDataStartCol = addSourceColumn ? 2 : 1; targetSheet.Range( targetSheet.Cells(destRow, nextDataStartCol), targetSheet.Cells(destRow + dataArr.length - 1, nextDataStartCol + totalCols - 1) ).Value2 = dataArr; if (addSourceColumn) { for (var r = 0; r < dataArr.length; r++) { targetSheet.Cells(destRow + r, 1).Value2 = filePath; } } destRow += dataArr.length; } // 第3步:关闭工作簿,不保存修改 wb.Close(false); } catch (e) { MsgBox("处理文件失败:" + filePath + "\n错误详情:" + e.description); continue; } } MsgBox("合并完成!共处理 " + files.length + " 个文件,汇总表总行数约 " + (destRow - 1) + " 行。"); } // ========== 自动扫描文件夹下的Excel文件 ========== function GetExcelFiles(folderPath) { var files = []; try { var fso = new ActiveXObject("Scripting.FileSystemObject"); var folder = fso.GetFolder(folderPath); var fileEnum = new Enumerator(folder.Files); for (; !fileEnum.atEnd(); fileEnum.moveNext()) { var f = fileEnum.item(); var ext = f.Name.toLowerCase(); if (ext.indexOf(".xlsx") > -1 || ext.indexOf(".xls") > -1 || ext.indexOf(".xlsm") > -1 || ext.indexOf(".csv") > -1) { files.push(f.Path); } } } catch (e) { MsgBox("无法自动扫描文件夹,请将配置区 manualFiles 改为 true,并手动填写文件路径。\n错误详情:" + e.description); return []; } return files; } // ========== 将Value2返回值统一转换为二维数组 ========== // WPS JS宏里,单个单元格返回标量,单行/单列返回一维数组, // 多行多列返回二维数组。这里统一转成二维数组,方便后续写入。 function normalizeTo2DArray(data, rowCount, colCount) { if (data == null) { var empty = []; for (var r = 0; r < rowCount; r++) { var rowArr = []; for (var c = 0; c < colCount; c++) { rowArr.push(""); } empty.push(rowArr); } return empty; } if (Array.isArray(data)) { // 已经是二维数组(数组的数组) if (data.length > 0 && Array.isArray(data[0])) { return data; } // 一维数组:如果行数为1,整体作为一行;否则每个元素作为一列 if (rowCount == 1) { return [data]; } else { var arrN = []; for (var i = 0; i < data.length; i++) { arrN.push([data[i]]); } return arrN; } } // 单个单元格/标量,填充整个区域 var arrS = []; for (var r = 0; r < rowCount; r++) { var rowS = []; for (var c = 0; c < colCount; c++) { rowS.push(data); } arrS.push(rowS); } return arrS; }以上代码可以直接复制到WPS的JS宏编辑器里使用。如果你运行时报MsgBox未定义,换成alert也是可以的,WPS的宏环境一般会兼容这两者。
4. 常见问题与排查技巧实录
4.1 问题速查表
下面这些是我在实际使用中遇到过的真实问题,整理成速查表,直接对照处理即可。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 运行后提示“没有找到任何Excel文件” | 文件夹路径不对,或路径末尾反斜杠缺失 | 检查folderPath,确认路径存在,且末尾为双反斜杠 |
| 宏按钮灰色,无法运行 | WPS未启用宏,或当前文件格式不支持宏 | 到“信任中心”开启宏;另存为支持宏的格式 |
ActiveXObject报错 | 系统安全策略禁止调用ActiveX组件 | manualFiles设为true,用手动文件路径数组 |
| 处理文件时弹“错误详情:文件已损坏” | 文件格式不是标准Excel,或文件正在被其他程序占用 | 确认要合并的文件都是可正常打开的工作簿;关闭正在编辑源文件的窗口 |
| 合并后列错位、数据串列 | 不同源文件的列顺序不一致,或存在隐藏列 | 先检查源文件表头结构,统一列顺序后再合并 |
| 只有第一个文件的数据出来了,后续文件没数据 | 后续文件表头行以外的数据行为空,或数据区域被识别为0行 | 检查源文件是否真的有数据;确认headerRows是否设置过大 |
| 运行很慢,像卡死一样 | 逐格读写单元格,或者文件数量过多 | 检查代码是否用了循环里Cells(r,c).Value2这种方式,应改为区域一次性读写 |
| 日期变成了数字串 | Value2保存的是日期序列值,不带显示格式 | 如需保留日期格式,改用Value读取,或在写入后设置单元格格式 |
| 合并单元格导致部分行丢失 | 源文件的表头或数据区有合并单元格 | 先取消合并单元格,保证每行数据对应独立单元格 |
4.2 避坑经验与技巧
第一条经验:永远先跑小批量测试。别第一次就直接合并所有文件。先复制两三个有代表性的文件到测试文件夹,比如一个有表头、一个没有数据、一个列顺序不同的文件,跑一遍看结果。确认脚本能正确处理这些“刁钻”情况后,再放全量文件。
第二条经验:保留“来源文件”列。之前我写过一个精简版合并脚本,为了图省事没加来源列,结果合并完发现某个数据对不上,完全不知道是哪来的。加上来源列之后,排查时间直接缩短到原来的十分之一。尤其是文件名称还带着日期、门店等信息时,这一列本身就是宝贵的数据源。
第三条经验:留意源文件里的隐藏行和隐藏列。UsedRange会把格式刷过的空白行也算进去,导致合并结果里出现大量空行。遇到这种文件,可以先在源文件里全选,按下Ctrl+End看看真正的数据边界在哪里,再决定是否需要做空行过滤。在我给的代码里,如果空行太多,可以再加一个简单的清洗步骤:读取数组后,跳过整行全是空值的那些行。
第四条经验:备份源文件。脚本虽然只读取不修改源文件,但运行前仍然建议把整个文件夹复制一份。毕竟操作的是几十个文件,万一代码里哪一行逻辑写错了,或者WPS在打开/关闭过程中出了问题,至少还有退路。
第五条经验:汇总表不要放在源文件文件夹里。不然扫描文件时,汇总表本身的临时备份文件可能被一起扫进去,造成同一个文件被合并进来甚至数据错乱。最好单独新建一个空工作簿来当汇总表,运行完再另存。
5. 扩展:从“能用”到“好用”
5.1 多Sheet合并与动态表头映射
我给出的是“每个文件只取第一个Sheet”的基础版本,但在真实业务里,有些工作簿的每个Sheet才是真正需要合并的对象,比如一个门店一个Sheet。这种情况其实不难改:把处理单个Sheet的代码提取成一个独立函数,在外层循环里再套一层for (var s = 1; s <= wb.Sheets.Count; s++),就能把所有Sheet都遍历一遍。要注意的只是汇总表里最好再加一列“来源Sheet”,否则数据一多,你根本分不清哪一坨是哪个Sheet的。
动态表头映射也是个很实用的升级方向。默认方案假设所有文件的列顺序一致,但现实中经常遇到“这个月表头多了一列,那个月底表头顺序不一样”的情况。如果要做得更健壮,可以在读取每个文件的表头后,和第一个文件的表头做匹配,按列名定位数据位置,再写入汇总表。这样即使列顺序打乱,也能按名对齐。这个逻辑需要多写一个列名映射函数,整体复杂度会上升不少,但对于长期维护的数据汇总任务非常值。
5.2 数据清洗与去重
合并之后通常还需要清洗数据。最常见的操作是去除完全重复的行、统一文本格式、把数字的千位分隔符去掉。这些操作可以在写入汇总表之前,对读取到的二维数组做预处理。比如“去重”可以用一个对象存“每一行拼接后的字符串”作为Key,遇到重复的Key就跳过。虽然这个方法在多列情况下可能误判,但大多数业务数据都够用。简单几行代码就能实现,不用引入外部依赖。
还有一个小技巧:如果源文件里有大量公式单元格,我用Value2读取到的是计算后的值,这通常是我们想要的。但如果你希望保留公式本身而不只是结果,那就得改成Value并调整写入方式。合并报表这种场景一般取结果值就够了,所以我个人不太推荐用Value,因为公式引用关系跨工作簿会变得很难维护。
5.3 把重复劳动做成一键操作
脚本跑通之后,你会发现真正花时间的不是合并本身,而是准备文件和确认结果。想更进一步的话,可以把这段宏保存成WPS的加载宏,甚至绑定到一个自定义按钮上,这样以后每次打开汇总工作簿,点一下按钮就能自动合并。
如果希望完全自动化,比如每天凌晨自动汇总前一天各个渠道发来的数据表,就可以把它和系统的任务计划程序结合起来,定时调用WPS来执行宏。具体做法是把要合并的文件夹固定下来,用命令行参数控制WPS打开汇总表并自动运行指定宏。这个方案配置起来稍微复杂一些,但一旦搭好,你连按钮都不用点了。当然,定时任务的稳定性、日志记录、异常通知这些都要提前想清楚,否则第二天早上一看,脚本半夜静默失败了,数据还是没合并上。
最后说一个我自己的习惯:这套合并脚本跑通之后,我都会在汇总表旁边加一个校验区,手动填“各文件行数之和”,再和实际合并结果的总行数做对比。总行数等于表头行数加上所有文件的数据行数之和,对不上就说明有文件被重复读取、漏读,或者存在空行干扰。脚本本身只是一个工具,真正让数据可靠起来的,是工具之外的校验意识。希望这份WPS JS宏批量合并Excel的方案,能真的帮你从重复劳动里解脱出来。