☰
WPS JS宏批量合并Excel:三步搞定几十个表格汇总
2026/10/23 1:17:13 网站建设 项目流程

WPS JS宏批量合并Excel这件事,听起来像是程序员才能碰的活,但当手头有几十个门店的销售报表、几十个部门的日数据、或者一堆从系统导出的分表需要攒成一张总表时,人力复制粘贴真的是最折磨人的操作。打开文件、全选、复制、切到汇总表、粘贴、关掉、打开下一个……机械重复半小时下来,眼睛花了不说,还容易漏行、串列,更别说把别人手工备注的散碎内容一起带进来。我第一次被这种活逼到崩溃后,直接用WPS自带的JS宏写了个脚本,整个过程拆成三步,几十个文件几秒钟全部合并完,还自带空行过滤和来源追溯。这篇博文就把这套方案完整拆开讲:三步合并法的思路、可以直接复制的完整代码、以及我在真实操作里踩过的各种坑。不管你是完全不懂代码的办公人员,还是想找个批量处理范本的初学者,都可以照着做。

1. 什么情况下值得为Excel合并写一个脚本

1.1 三个典型的合并场景,基本覆盖掉日常大多数需求

先说三个我实际碰到过、也帮周围人处理过的场景,你看看是不是似曾相识。

第一个是定期报表汇总。这类最常见:公司下面有十几家门店或者几十个销售小组,每个单位每周、每月发一张固定格式的Excel表报上来,负责人需要把所有表按照相同列结构竖向堆到一起,变成一张总明细。刚开始门店少,手粘一下还能忍,等到二十家以上就完全是体力活了。

第二个是系统导出的分片数据。很多业务系统导出数据时会按时间、按地区、按订单来源切分文件。比如每天导出一个Excel文件,月底要合并成一张全量明细表。这类文件列结构一模一样,只是行数不同,人工合并不仅慢,还容易在反复切换窗口时丢失数据。

第三个是数据补录和汇总分析。有时候你拿到的数据分散在多个工作簿里,每张表又可能不止一个工作表,分析前需要先把它们集中到一张表里统一处理。这种活通常还要反复做——比如每次跑数都要重新合并一次,每次合并的操作方式完全一样。

这些任务的共同特征很明确:格式统一、规则简单、量大人烦。判断标准就一句话:你操作时已经不需要思考,只是机械重复,那这件事就肯定可以交给脚本。

1.2 为什么选JS宏而不是VBA或者Python

有人可能会说,合并Excel用Python写个pandas脚本不就行了?确实行,但放到真实办公环境里,问题常常不在代码本身,而在环境。我很直白地讲,以下是我推荐WPS JS宏的理由:

对比项WPS JS宏VBAPython + pandas
运行环境WPS自带,零安装WPS兼容模式下可用需要安装Python环境和库,公司电脑经常不允许
使用门槛懂一点JS或直接抄代码就能跑语法风格偏老,空行、对象引用处理繁琐需要管理路径、虚拟环境、命令行,上手门槛高
与表格的联动直接操作当前打开的WPS表格同样可以直接操作通常要打开/保存外部文件,操作不直观
适合人群办公人员、非技术岗老Office开发者数据工程师、专业分析师

更重要的是,WPS新版本主推的宏语言就是JavaScript,语法和前端JavaScript高度一致,字符串处理、数组操作、正则表达式都比VBA顺手太多。比如判断一个单元格是否为空,在JS里可以用String(v).trim(),在VBA里则要处理各种Null和Trim的组合问题。对于只想要快速把工作搞定的人来说,JS宏是学习成本和可使用度最平衡的选择。

2. 动手前必须搞懂的JS宏对象模型和运行环境

2.1 四个层级看懂所有Excel脚本操作

写WPS的JS宏之前,先不用急着背一堆API,把对象模型搞清楚,你就能看懂绝大部分代码。WPS的层级关系可以理解成下面这样:

Application(WPS程序本身) └ Workbooks(所有打开的工作簿集合) └ Workbook(一个Excel文件) └ Worksheets(这个文件里的所有工作表集合) └ Worksheet(某一张工作表) └ Range / Cells(单元格区域 / 单元格)

用生活里的例子比喻:工作簿就是桌面上的档案夹,工作表是档案夹里的那张表格纸,单元格是表格纸上一个一个的小格子。每次你操作Excel表格,本质上都是顺着这条层级链,找到某一个具体格子,然后读它、写它或者改它的格式。

日常写代码时,你至少需要区分这几个对象:

  • ActiveWorkbook:当前正在操作的工作簿。
  • ThisWorkbook:宏代码所在的那个工作簿。
  • Workbooks.Open(路径):打开一个外部Excel文件,并返回这个文件对应的Workbook对象。
  • wb.Close(false):关闭打开的工作簿,false表示不保存修改。
  • Worksheets.Item(1):取第一张工作表,也可以按名字取,比如Worksheets.Item("数据")。
  • Cells.Item(行, 列):按行号和列号定位单元格,比如Cells.Item(2, 3)就是第2行第3列,也就是C2。
  • UsedRange:一个工作表里所有被使用过的区域,常用来快速算出数据的最后一行和最后一列。

2.2 宏的入口、保存格式和第一次运行

在WPS里写JS宏,入口比较隐蔽,这里直接按我习惯的路径走一遍。

先打开任意一个WPS表格文件,在菜单栏找到“开发工具”选项卡。如果你压根没看到这个选项卡,就去“文件 → 选项 → 自定义功能区”,在右侧勾选“开发工具”。勾完以后菜单栏就会出现,这一步比较容易漏。

点击“开发工具”里的“WPS宏编辑器”,或直接按快捷键Alt + F11,就能进入宏编辑界面。左侧面板会显示当前工作簿的项目结构,右侧就是代码输入区。写宏不是直接在原文件里保存,而是要把文件“另存为”成启用宏的工作簿格式,也就是.xlsm。如果你存成普通的.xlsx,下次打开宏代码会直接丢失,这个我一开始就吃过亏。

第一次运行宏时,WPS通常还会弹一个关于启用宏的安全提示。如果你看不到任何和宏相关的按钮、代码编辑窗口,很大概率是宏被安全策略禁用了,去“文件 → 选项 → 安全”或“信任中心”里找到“宏设置”,选择“启用所有宏”或类似选项。

2.3 写个最小测试宏,先验证环境是不是通

正式写大脚本之前,强烈建议先写一个最小的测试宏,确保宏编辑器、运行环境都正常。直接在代码区粘贴下面这段:

function 测试宏() { let sheet = ActiveWorkbook.Worksheets.Item(1); sheet.Range("A1").Value2 = "宏脚本测试成功"; 弹窗("当前工作表是:" + sheet.Name); } function 弹窗(text) { try { MsgBox(text); } catch (e) { alert(text); } }

运行方式是:光标放在测试宏函数里,点击编辑器上的运行按钮,或者直接按F5。运行后,当前工作簿的第一张表A1单元格会写上“宏脚本测试成功”,同时弹出一个消息框显示当前工作表名。这里顺手写了一个弹窗兼容函数,是因为不同WPS版本里MsgBox和alert的支持情况不太一样,两个都试一下,保证在你环境里至少有一个能弹出来。

这个最小宏跑通,说明你的宏编辑器、执行权限、代码基础环境都没有问题。很多人装完环境就直接上大脚本,结果核心代码还没跑就被环境问题卡住,反而效率更低。

3. 三步合并法的完整代码实现:选文件、读数据、写汇总

3.1 第1步:弹窗选文件,比写死路径可靠得多

网上很多教程喜欢把要合并的文件路径写死在代码里,比如“C:\某文件夹\1.xlsx”。但实际工作中,文件路径隔一段时间就会变、文件夹名字可能改、月份目录也不一样,写死路径意味着每次都要改代码,反而更麻烦。

我的做法是直接用文件选择对话框让用户手动多选文件。核心代码长这样:

let fd = Application.FileDialog(3); // 3 代表文件选择对话框 fd.AllowMultiSelect = true; // 允许按住Ctrl多选 fd.Title = "请选择要合并的Excel文件"; fd.InitialFileName = ThisWorkbook.Path + "\\"; if (fd.Show() == 0) { return; // 用户点了取消,直接退出 } let files = fd.SelectedItems; // 用户选中的所有文件路径集合

这里有一个新手非常容易踩的细节:files虽然看起来像数组,但它其实是一个Office风格的集合对象,索引从1开始,不是从0开始。后面写循环时要从i = 1开始遍历,而不是i = 0。我先提一句,后面第4章还会展开讲。

另外,fd.InitialFileName = ThisWorkbook.Path + "\\"的意思是,打开文件选择框时默认定位到宏所在工作簿的目录。如果你已经把所有待合并文件都放在同一个文件夹里,这个默认位置能省不少事。

3.2 第2步:逐文件打开并找出有效数据区域

文件选择完毕,真正的合并工作就开始了。核心逻辑是:逐个打开Excel文件,读取第一个工作表,找到有效数据的最后一行和最后一列,然后从第2行开始逐个单元格取值。

判断有效区域用的是UsedRange,它返回工作表里有内容的区域,但理解它的计算逻辑很重要。UsedRange本身有4个关键属性:

  • Row:有效区域的起始行号
  • Column:有效区域的起始列号
  • Rows.Count:有效区域的总行数
  • Columns.Count:有效区域的总列数

所以有效区域的最后一行可以这样算:

let srcUsed = sourceWs.UsedRange; let lastRow = srcUsed.Row + srcUsed.Rows.Count - 1; let lastCol = srcUsed.Column + srcUsed.Columns.Count - 1;

为什么要减1?因为Row + Rows.Count - 1才是结束行。比如有效区域从第1行开始,总共有5行,那最后一行就是1 + 5 - 1 = 5。这个逻辑不弄清楚,代码很容易在边界条件上出错。

定位到有效区域之后,从第2行开始遍历。因为绝大多数表第1行是表头,我们要跳过表头,只把数据行合并进去。遍历每一行时,还要先判断这一行是不是空行。实际表格里经常有人无意中点到空格、设置了格式,导致一行明明看着是空的,却仍然被UsedRange计数。如果不做空行过滤,合并结果会出现几百上千条空白行。

3.3 第3步:把数据写入汇总并收尾

写入环节的核心是维护一个不断递增的targetRow变量。汇总表第1行是表头,所以数据从targetRow = 2开始写。每写完一行数据,targetRow加1,接着写下一行。

写完后要调用wb.Close(false)关闭源工作簿。这里的false表示不保存修改,因为你只是读取数据,改动源文件没有任何意义。很多初学者只记得打开、忘了关闭,导致脚本跑完以后,系统里残留一大堆Excel进程,第二次运行就明显卡顿。

全部处理完,用兼容弹窗函数提示“合并完成,共汇总多少行”,这样你心里有数,能快速核验结果。

3.4 可以直接复制的完整代码

下面是完整版代码,我已经把上面三步连贯起来了。把这段代码复制到WPS宏编辑器里,保存成.xlsm,运行批量合并Excel()函数即可。

// ============================================ // 功能:批量合并Excel文件(取每个文件的第一个工作表) // 用法:将代码粘贴到WPS宏编辑器,运行“批量合并Excel” // 说明:所有源文件默认第1行是表头,第2行起是数据 // ============================================ function 弹窗(text) { try { MsgBox(text); } catch (e) { alert(text); } } function 批量合并Excel() { let startTime = new Date().getTime(); // ---------- 第1步:弹窗选择要合并的文件 ---------- let fd = Application.FileDialog(3); // 3 表示文件选择对话框 fd.AllowMultiSelect = true; fd.Title = "请选择要合并的Excel文件(可多选)"; fd.InitialFileName = ThisWorkbook.Path + "\\"; if (fd.Show() == 0) { return; // 用户点击取消 } let files = fd.SelectedItems; if (files.Count == 0) { return; } // ---------- 第2步:准备汇总表 ---------- let summarySheet = ActiveWorkbook.Worksheets.Item(1); // 默认从汇总表第2行开始写。 // 如果这张表之前已经有合并结果,建议先手动清空旧数据再运行。 let targetRow = 2; // ---------- 第3步:遍历文件,读取数据写入汇总 ---------- for (let i = 1; i <= files.Count; i++) { let filePath = files.Item(i); // 跳过宏所在工作簿自己,避免把汇总表自身数据再合并进来 if (filePath.toLowerCase() === ThisWorkbook.FullName.toLowerCase()) { continue; } let wb = Workbooks.Open(filePath); let sourceWs = wb.Worksheets.Item(1); // 取每个文件的第一个工作表 // 计算源文件有效区域的最后一行和最后一列 let srcUsed = sourceWs.UsedRange; let lastRow = srcUsed.Row + srcUsed.Rows.Count - 1; let lastCol = srcUsed.Column + srcUsed.Columns.Count - 1; // 从第2行开始,跳过表头 for (let r = 2; r <= lastRow; r++) { // 跳过整行都为空的脏数据行 let isEmptyRow = true; for (let c = 1; c <= lastCol; c++) { let v = sourceWs.Cells.Item(r, c).Value2; if (v !== null && String(v).trim() !== "") { isEmptyRow = false; break; } } if (isEmptyRow) { continue; } // 把这一行的每一列写入汇总表 for (let c = 1; c <= lastCol; c++) { summarySheet.Cells.Item(targetRow, c).Value2 = sourceWs.Cells.Item(r, c).Value2; } targetRow++; } wb.Close(false); // 数据已读完,不保存关闭源文件 } let elapsed = (new Date().getTime() - startTime) / 1000; 弹窗("合并完成,共汇总 " + (targetRow - 2) + " 行,耗时 " + elapsed.toFixed(1) + " 秒"); }

使用步骤我建议按下面这个顺序走:

  1. 在WPS里打开一个空白工作簿,另存为.xlsm格式。
  2. 按快捷键Alt + F11打开宏编辑器,新建一个脚本模块,把上面代码粘贴进去。
  3. 保存代码,回到WPS表格界面。
  4. 按Alt + F8调出宏列表,找到批量合并Excel,点击运行。
  5. 在弹出的文件选择框里选中所有要合并的文件,点确定。

第一次运行时,先选两个小文件试跑,确认输出结果符合预期后,再真正处理全量文件。千万不要一上来就选几十个文件跑,那样出了问题都不知道哪里错。

4. 实测阶段踩过的坑:集合下标、文件占用、脏数据一个不少

4.1 集合下标从1开始,别拿JavaScript的数组习惯硬套

这是我第一次写WPS JS宏时实实在在卡住的问题。写惯了前端JavaScript的人,遍历任何集合第一反应都是写for (let i = 0; i < files.Count; i++),然后取files[i]。但WPS的SelectedItems是Office风格的集合对象,不是原生数组,它的第一项是files.Item(1)而不是files[0]。如果你从0开始循环,轻则漏掉第一个文件,重则直接抛“下标越界”。

解决办法很简单:统一用for (let i = 1; i <= files.Count; i++),并且用files.Item(i)来取值。这个习惯一旦养成,后面接触Worksheets集合、Workbooks集合时就不会再犯同类错误。

4.2 打开的工作簿一直不关,第二次跑就卡得要死

真实的办公电脑配置一般,如果一次合并三四十个文件,每个文件都通过Workbooks.Open打开,而不调用wb.Close(false)关闭,那么跑完一次脚本后,任务管理器里会挂着一大串WPS后台进程。第二次再跑,电脑直接卡到鼠标飘,严重的时候还会提示内存不足。

这个问题不是写代码时立刻暴露的,而是跑了几次以后才逐渐明显。我的建议是:在循环代码中保持wb.Close(false)与Workbooks.Open成对出现。更进一步,可以用try...catch...finally结构,把wb.Close(false)放到finally块里,这样就算读取某一行时报错,也能保证已经打开的工作簿被关闭,不会发生“一个文件出错,后面所有文件全卡死”的情况。

大致的容错结构可以这样:

let wb = null; try { wb = Workbooks.Open(filePath); // 读取数据并写入汇总表 } catch (err) { 弹窗("处理失败:" + filePath + ",原因:" + err.message); } finally { if (wb !== null) { wb.Close(false); } }

4.3 不清理旧汇总数据,行数会越跑越多

假设你跑了一次合并,汇总表已经有500行数据。改了一点代码,或者想重新算一遍,又跑了一次脚本。由于脚本默认targetRow = 2,第二次运行会把新数据从第2行开始覆盖写入,理论上不会出问题。但如果两次合并的文件数量不一样、源文件有新增内容,或者上一次跑完以后有人在汇总表里手动加过内容,就很容易出现新旧数据混在一起的情况。

保险的做法是在运行前清空汇总表的数据区域。如果你已经确定汇总表结构,可以直接手动清空旧数据再跑。如果想集成到代码里,可以加一段自动清理逻辑,从第2行扫到最后一个有内容的行,然后清除内容。我这里给一个示例片段,你可以自行嵌到主代码的“准备汇总表”部分:

let su = summarySheet.UsedRange; let oldLastRow = su.Row + su.Rows.Count - 1; if (oldLastRow >= 2) { summarySheet.Range( summarySheet.Cells.Item(2, 1), summarySheet.Cells.Item(oldLastRow, 20) ).ClearContents(); }

注意这个片段固定清理到第20列,如果你的表格列数超过20,需要把数字调大。更严谨的做法是先读取汇总表表头的列数,再根据列数构造清理范围,这里为了篇幅不展开了。

4.4 源表里那些看不见的脏数据,会让结果多出几千个空行

合并脚本写好后,我发现有一次跑出来的总表有将近3000行,但源文件的数据加起来应该只有800多行。排查了很久,最后定位到其中一个源文件。打开那个文件按Ctrl + End,光标直接跳到了第3000行,但屏幕上明明只有几十行有数字。这就是典型的格式残留:有人曾经在几百行里填过内容又删掉,但格式、边框、空格符号还残留在单元格里,UsedRange会把它们全部算进去。

这类问题靠肉眼是发现不了的,只有脚本跑完看汇总行数才会暴露。前面完整代码里的“空行过滤”已经能把大部分假空行挡掉,但如果你按Ctrl + End发现某张表的光标停在很后面的位置,最好源头处理一下:全选那些假空行,右键“删除”而不是“清除内容”,然后保存,再重新合并。

4.5 文件被占用、宏环境差异,这些偶发问题怎么兜底

办公环境里最现实的问题是:你正跑着脚本,结果某个Excel文件正在被另一个同事打开。这种情况下Workbooks.Open往往会以只读方式打开,或者弹出一个文件占用提示,导致脚本卡在那里不动。代码层面可以加一个简单的只读判断:

if (wb.ReadOnly) { 弹窗("注意:" + filePath + " 被只读打开,请确认文件未占用"); wb.Close(false); continue; }

另一个偶发问题是不同WPS版本的API差异。有些旧版本对MsgBox、FileDialog的支持和预期不太一样。解决方案就是我前面说的:尽量使用通用的数字枚举、写兼容函数,不要在一个版本上跑通就以为所有环境都能通。至少在同一台电脑上部署前,先用最小测试宏验证一遍基础环境。

5. 给合并脚本做的升级改造:来源文件名、Sheet匹配、批量提速

5.1 在汇总表里加上“来源文件名”这一列

合并完成以后,最尴尬的问题就是:某一行数据出错了,想查它到底来自哪个文件,结果所有数据混在一起根本分不清。如果合并源头有几十个文件,想定位问题行都无从下手。

所以我的建议是,在汇总表右侧加一列“来源文件”,每写完一行数据,顺便把当前工作簿的文件名写进去。实际改造代码时,你可以在写入每一行数据的内层循环后面加上这样一句:

let sourceFileName = wb.Name; // 例如“门店A.xlsx” let sourceColumn = lastCol + 1; // 放在数据列后面单独一列 summarySheet.Cells.Item(targetRow, sourceColumn).Value2 = sourceFileName;

这样每一条汇总记录都能追溯到源文件,后期核对数据、找异常都要方便得多。你还可以进一步写来源表名、来源行号,但一般文件名就够了。

5.2 不写死第一个Sheet,按Sheet名找到真正的数据表

基础版代码写的是“取每个文件的第一个工作表”。但在实际工作中,有些文件的数据根本不在第一个Sheet,或者第一个Sheet是封面、说明页,真正数据在第二三张表里。如果一刀切取第一个Sheet,合并出来的数据就是乱的。

更稳妥的逻辑是:优先查找名字匹配“数据”“明细”“本月”这类关键词的工作表,找不到再退回第一个工作表。示例代码如下:

function 找数据表(wb) { let totalSheets = wb.Worksheets.Count; for (let s = 1; s <= totalSheets; s++) { let sheetName = wb.Worksheets.Item(s).Name; if (sheetName.indexOf("数据") > -1 || sheetName.indexOf("明细") > -1) { return wb.Worksheets.Item(s); } } return wb.Worksheets.Item(1); }

然后在主循环里把wb.Worksheets.Item(1)替换成找数据表(wb)就行。注意indexOf匹配范围可以自己按实际表的名字调整,核心思想是“智能匹配优先,固定第一个Sheet兜底”。

5.3 大数据量时的提速思路:用Range批量读写

基础版本里,我用了两层循环逐格读取单元格:外层遍历行、内层遍历列。这种写法对几千行、十几列的数据完全够用,但如果你要合并的是几万行、几十列的大表,就会发现速度明显变慢。原因很简单:每次通过Cells.Item(r, c)访问单元格,脚本引擎都要和表格对象做一次交互,交互次数越多,整体耗时越长。

提速的基本思路是把整块区域一次性读成一个二维数组,在内存里处理完,再一次性写回汇总表。核心写法是:

let data = sourceWs.Range( sourceWs.Cells.Item(2, 1), sourceWs.Cells.Item(lastRow, lastCol) ).Value2;

这样拿到的data就是二维数组,第一维是行,第二维是列。之后遍历数组、过滤空行,速度比逐个读写单元格快几个量级。写回汇总表时,也可以先构造好目标区域,再一次性赋值。不过要提醒一下:不同WPS版本对Range.Value2返回数组的结构可能略有差异,单行单列时尤其容易变成一维数组或标量。建议在小数据量上先验证,确认取到的类型符合预期后再全量跑。

如果你不需要追求极限性能,基础版的逐格读写完全够用。提升性能之前,先想想你的数据量是不是真的到了需要优化的程度。

5.4 把脚本绑定到按钮,一键运行

脚本写好后,别人不一定知道怎么打开宏编辑器、怎么运行宏。为了让整个流程傻瓜化,我把这个合并功能绑定到了表格里的一个按钮上:在WPS菜单的“开发工具”里选择“插入表单控件”或“插入形状”,随便画一个矩形框,右键点击矩形框,选择“指定宏”,然后选中批量合并Excel函数。保存后,别人只需要打开这个工作簿,点一下按钮,整个合并过程就完成了,完全不用看到一行代码。

如果你的Excel汇总任务是要交给同事使用的,这个按钮设计几乎是必须的。不然你写好的脚本只能在自己电脑上跑,换到别人电脑上,对方连宏编辑器在哪里都不知道。

最后说点实际操作中的体验

这个脚本我自己用了很长一段时间,最大的感受是:合并Excel这件事,真正难的从来不是写代码,而是把规则想清楚。比如要不要保留表头、遇到空行怎么处理、源文件格式不统一时怎么兜底、跑完以后怎么核验结果,这些想明白了,代码反而是水到渠成的事。最开始跑脚本之前,我建议一定先备份原始数据和汇总表。脚本跑完后,不要只盯着“共汇总多少行”那个提示,花几分钟抽样看几条明细,确认数据真的逐行对齐了。还有一个小技巧:把汇总文件固定命名后放在固定目录,每次只需要选择当月新增的文件运行,不要总是全选整个文件夹,这样既省时间,也减少把无关文件合进来的概率。合并脚本这种东西,属于写一次就能反复用的资产,值得你花半小时把它打磨到顺手。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询