资讯动态

WPS JS宏三步实现跨工作簿Excel批量合并,附完整代码

发布时间:2026/10/23 3:49:24 来源:尧图企业网站定制
上周帮一位做行政的同事处理数据汇总她面对的是三十多个分散在不同电脑上的Excel文件考勤、加班、绩效各一摊格式还互不相同。她复制粘贴了一下午眼睛发花错位了三行数据最后只能整体返工。这个场景做财务、行政、运营、数据分析的朋友大概率都经历过——跨工作簿数据汇总听起来不难手动操作却极其折磨人。我给她写了一段WPS JS宏三分钟跑完全部合并准确率百分百。今天把这段脚本完整拆开讲清楚用WPS JS宏批量合并Excel文件的思路和坑三步实现跨工作簿数据汇总文末附可直接复制的完整代码。1. 为什么是JS宏先想清楚方案选型1.1 手动汇总的痛点和需求边界在动手写代码之前先明确一下这类需求的真实边界。日常办公里的“批量合并”往往不是技术难题而是体力活把多个工作簿里的数据贴进同一个表然后统一排版、统一口径。量少的时候无所谓一旦超过十个文件、每个文件上千行手动的错误率会急剧上升而且几乎不可能发现。我见过太多人用“全选复制、粘贴到汇总表”的方式处理最后得到的汇总表经常出现三种问题粘贴的列顺序错位因为两张表的列标题顺序不一致重复的表头混在数据中间日期、数字变成了文本格式后续求和统计全部失效。这些问题的根源都一样人的注意力在这种机械操作中撑不了那么久。所以一个真正能落地的批量合并方案核心不是“能合”而是稳定、可重复、出错可控。这就对技术路线提出了要求它必须能自动遍历文件能统一处理表头还要能保留原始的数据格式和内容。1.2 三条技术路线VBA、JS宏、第三方工具针对“跨工作簿汇总”这个需求市面上能走的路主要有三条。第一条是VBA宏。VBA在Excel生态里是元老级的存在网上能找到大量现成代码。但它有个现实问题很多个人版WPS用起来总差口气宏功能受限老代码跑起来各种弹窗、兼容性警告。另外VBA的语法对现在的办公人员来说有点老旧他们很多已经习惯了JavaScript那套思维方式。第二条是各种第三方Excel合并工具。这类工具胜在零门槛点点鼠标就能合。但代价是格式控制弱很多工具合并后只保留值丢掉样式公式直接变成静态结果。而且第三方的数据安全是个隐忧上传到云端的工具更麻烦很多公司现在对办公数据外传非常敏感。第三条也是我推荐的路线就是WPS JS宏。WPS从某个版本开始内置了JavaScript宏环境直接复用大家的JS知识在WPS里原生运行本地执行不依赖网络个人版也能用。语法上写起来像在写前端逻辑有对象、有数组、有循环跟VBA那套面向集合的写法完全是两种体验。对于“遍历文件夹、循环打开文件、逐行读取、写回汇总表”这种套路化任务JS宏的表达反而更顺手。从成本和可控性来看我最终选了JS宏。它能原生调用WPS的表格对象模型又能用JS的数组和循环逻辑去处理流程两头优势都占。1.3 WPS JS宏的入口与动作如果你还没用过WPS的JS宏第一步得先把开发工具调出来。WPS表格的菜单栏上默认不一定显示“开发工具”选项卡。调出来的路径各版本的入口大同小异通常是在“文件 → 选项 → 自定义功能区”里把“开发工具”勾上确定之后菜单栏就会出现。点进去之后你会看到“切换到VBA环境”和“切换到JS环境”一定要选JS环境否则粘贴JS代码是跑不起来的。新建模块之后左侧会出现一个代码编辑区这里就是写脚本的地方。运行的时候把光标放在函数内部点工具栏上的运行按钮或者直接在编辑器里按下快捷键WPS就会开始执行。新手最容易踩的坑是代码粘贴进去之后点击运行没反应。原因九成是环境没切换对——还在VBA环境里。这个等下在踩坑章节我还会细说。2. 三步合并的整体设计先把逻辑讲透2.1 三步法的核心拆解标题里说的“3步”对应到技术实现上就三件事枚举文件、读取数据、写出汇总。这三件事每一步都不复杂但每一步都有细节少了任何一个细节合并结果就可能出问题。第一步枚举文件。程序需要自动找到目标文件夹里所有的Excel文件而不是你手动告诉它文件名。这靠的是文件系统遍历一个目录扫进去把所有匹配后缀的文件路径收集起来。第二步读取数据。打开一个文件找到指定的工作表把数据区域读取出来。这里的“数据区域”判断是重点——哪些行是表头哪些行是数据列数在哪里结束这些都是这一步要回答的问题。第三步写出汇总。把所有文件的数据行追加到汇总表里然后统一保存成一个新文件。这三步串起来就是一个完整的流水线。设计上我把每一步做成了独立的代码块这样以后改动起来也好维护。你只想合并特定的几个Sheet只需要动第二步你想把输出格式改成CSV只需要动第三步。2.2 几个关键设计决策在写代码之前有几个设计决策要先说清楚它们是这套方案默认的“公约”。第一个决策是表头约定。我的方案约定每个源文件的第一行是表头合并时只保留第一个文件的表头其余文件的表头行跳过。这是大多数日常需求的做法。如果某个文件没有表头你需要在配置区里把表头行数改为0程序会分别处理。第二个决策是列数自适应。不同的文件列数可能不一样程序不能固定列宽。设计上以“已有文件中的最大列数”为汇总表的列数不足的单元格留空。要注意的是这种合并不是按列名匹配的关联合并而是按位置堆叠。也就是说所有源文件的列顺序必须一致。如果有两张表列名一样但顺序不同合并后会错位这不是程序能自动纠正的需要你先手工统一列顺序。第三个决策是格式保留策略。读取单元格值的时候用一个统一读取的方式拿到单元格的显示值而不是底层对象。这样日期、数字、文本都能以“看起来的样子”进入汇总表不会丢格式。代价是如果源单元格里有公式拿到的将是计算后的结果而不是公式字符串。对于汇总表来说这通常正是你需要的。第四个决策是性能策略。最笨的写法是每个单元格都读写一次文件多的时候会慢到怀疑人生。我采用的是批量读取加逐行写入的方式先读取整个数据区域到一个临时数组然后逐行写入汇总表。这样每次打开文件只做两次大的读写操作比一个格子一个格子地操作快一个数量级。2.3 目录规划与准备工作代码跑起来之前目录规划能避免一半的麻烦。我给同事做这个案例的时候让她先把所有待合并的Excel文件放到同一个文件夹文件夹里除了这些源文件什么别的文件都不放。为什么强调这个因为程序是按后缀名扫描文件的如果文件夹里混入了临时文件、备份文件也会被扫进来最终结果就会多出不该有的数据。如果实在没办法避免可以在代码里加一个文件名排除规则比如“以~$开头的临时文件过滤掉”。另外要注意路径写法。Windows的路径是反斜杠但在JS字符串里反斜杠是转义符。两个解决方案使用正斜杠写路径比如D:/项目数据/待合并/或者在写路径的时候写成双反斜杠“\”。我习惯用正斜杠代码里看起来更干净。还有就是路径尽量避免带空格和中文之外的生僻字符某些老版本在路径编码上处理得不好容易读取失败。输出文件建议放在源文件目录的上一级或者专门建一个输出目录。这样源文件目录保持“纯净”下次再运行脚本时扫描范围还是正确的。3. 参考实现分步写出完整代码3.1 第1步配置参数与批量枚举文件第一步代码要做两件事读取配置信息扫描目录。我习惯在脚本开头列一个“配置区”把所有可能需要修改的参数集中放在这里用户拿到脚本后只需要改这一段不用翻代码。配置区里至少要有三项源文件目录路径、表头行数、数据起始行。下面这段是枚举文件的实现// 配置区 var folderPath D:/项目数据/待合并文件/; // 源文件目录注意用正斜杠 var headerRows 1; // 每个文件的表头行数 var startRow 2; // 数据起始行表头1 // 扫描目录 var fs new ActiveXObject(Scripting.FileSystemObject); if (!fs.FolderExists(folderPath)) { MsgBox(目录不存在 folderPath); return; } var folder fs.GetFolder(folderPath); var files new Enumerator(folder.Files); var fileList []; while (!files.atEnd()) { var f files.item(); var fileName f.Name.toLowerCase(); // 只收集Excel文件同时过滤临时文件 if ((fileName.indexOf(.xlsx) -1 || fileName.indexOf(.xls) -1) fileName.indexOf(~$) -1) { fileList.push(f.Path); } files.moveNext(); } if (fileList.length 0) { MsgBox(目录下没有Excel文件); return; } MsgBox(发现 fileList.length 个文件);这里有一个关键点文件对象遍历用的Enumerator是WPS环境提供的跟浏览器的Iterator不太一样。你自己写循环时注意不要用数组的forEach因为这里拿到的不是数组是一个迭代器对象。3.2 第2步读取源文件并组装数据第二步是整个脚本的核心打开工作簿读取数据区域逐行写入汇总表。这里我把读取和写入拆成两个独立的逻辑方便你按需调整。// 新建汇总表 var outBook Workbooks.Add(); var outSheet outBook.Sheets.Item(1); outSheet.Name 汇总数据; var rowOffset 1; // 汇总表当前写入行 var maxCols 0; // 动态计算最大列数 // 遍历文件 for (var i 0; i fileList.length; i) { Application.StatusBar 正在合并 (i 1) / fileList.length; var wb Workbooks.Open(fileList[i]); var ws wb.Sheets.Item(1); // 默认取第一个Sheet var used ws.UsedRange; // 数据区域 var rows used.Rows.Count; var cols used.Columns.Count; if (cols maxCols) maxCols cols; // 更新最大列数 // 第一个文件包含表头后续文件跳过表头 var dataStart (i 0) ? 1 : startRow; if (headerRows 0) dataStart 1; // 无表头文件统一从第1行开始 // 逐行逐列读取并写入汇总表 for (var r dataStart; r rows; r) { var isEmpty true; for (var c 1; c cols; c) { var cellValue used.Cells(r, c).Value2; outSheet.Cells(rowOffset, c).Value2 cellValue; if (cellValue ! null cellValue ! ) { isEmpty false; } } // 整行为空时跳过不占汇总表行号 if (!isEmpty) { rowOffset; } } wb.Close(false); // 关闭源文件不保存 }这个循环里有三个容易出错的地方。第一个是Worksheet对象引用打开一个工作簿之后必须用一个变量接住后续操作都用这个变量而不是用ActiveWorkbook或ActiveSheet。因为循环里文件一个接一个打开活动对象是动态变化的直接引用活动对象会在文件切换时指向错乱的表。第二个是整行空值判断。我检查了每一行的所有列如果该行所有单元格都为空说明这是一条空行跳过它。这是为了避免源文件里的空行把汇总表撑得很松散。但要注意这个逻辑只适合“纯空行”如果一行里中间某列有空格字符串会被判定为非空。第三个是关闭源文件的方式。wb.Close(false)里的false表示不保存非常重要。因为打开文件只是读取数据如果误触发保存会把源文件覆盖成别的内容这是毁灭性事故。3.3 第3步写出汇总表并保存结果数据写完之后汇总表还在内存里需要保存到磁盘。这一步我做了两件附加的事补全表头区域以及保存文件。// 保存汇总表 var outputPath folderPath 汇总结果.xlsx; // 判断输出文件是否已存在 if (fs.FileExists(outputPath)) { fs.DeleteFile(outputPath); } outSheet.Cells(1, maxCols 1).Value2 ; // 占位避免表头区域只有前几列有值 outBook.SaveAs(outputPath); outBook.Close(false); Application.StatusBar false; MsgBox(合并完成共写入 (rowOffset - 1) 行数据输出文件 outputPath);这里有一个容易被忽略的细节在滚动到第1行第maxCols1列写入值是为了保证汇总表的最大列宽和源数据一致。否则在极端情况下当第一个文件只有5列而后续文件有8列时表头区域可能不会自动扩展到8列但数据区域已经写满了8列导致表头和数据错位。保存之前先检查输出文件是否已存在。如果存在直接删除旧文件再保存新的。这样做比覆盖保存更干净能避免“文件被占用”“格式不匹配”等隐蔽问题。3.4 可直接复用的完整脚本前面三节是分步解析这里我给出一个整合后的完整脚本。你把下面这段整体复制到WPS JS宏编辑器里修改开头的文件夹路径然后运行即可。// 配置区 var folderPath D:/项目数据/待合并文件/; // 改成你的目录 var headerRows 1; // 表头行数 var startRow 2; // 数据起始行 // 主函数 function 批量合并Excel文件() { // 扫描目录 var fs new ActiveXObject(Scripting.FileSystemObject); if (!fs.FolderExists(folderPath)) { MsgBox(目录不存在 folderPath); return; } var folder fs.GetFolder(folderPath); var files new Enumerator(folder.Files); var fileList []; while (!files.atEnd()) { var f files.item(); var fileName f.Name.toLowerCase(); if ((fileName.indexOf(.xlsx) -1 || fileName.indexOf(.xls) -1) fileName.indexOf(~$) -1) { fileList.push(f.Path); } files.moveNext(); } if (fileList.length 0) { MsgBox(目录下没有Excel文件); return; } // 新建汇总表 var outBook Workbooks.Add(); var outSheet outBook.Sheets.Item(1); outSheet.Name 汇总数据; var rowOffset 1; var maxCols 0; // 遍历文件 for (var i 0; i fileList.length; i) { Application.StatusBar 正在合并 (i 1) / fileList.length; var wb Workbooks.Open(fileList[i]); var ws wb.Sheets.Item(1); var used ws.UsedRange; var rows used.Rows.Count; var cols used.Columns.Count; if (cols maxCols) maxCols cols; var dataStart (i 0) ? 1 : startRow; if (headerRows 0) dataStart 1; for (var r dataStart; r rows; r) { var isEmpty true; for (var c 1; c cols; c) { var cellValue used.Cells(r, c).Value2; outSheet.Cells(rowOffset, c).Value2 cellValue; if (cellValue ! null cellValue ! ) { isEmpty false; } } if (!isEmpty) { rowOffset; } } wb.Close(false); } // 保存 var outputPath folderPath 汇总结果.xlsx; if (fs.FileExists(outputPath)) { fs.DeleteFile(outputPath); } outSheet.Cells(1, maxCols 1).Value2 ; outBook.SaveAs(outputPath); outBook.Close(false); Application.StatusBar false; MsgBox(合并完成共写入 (rowOffset - 1) 行数据输出文件 outputPath); }直接复制注意一点WPS的JS宏编辑器默认是ES3风格部分新特性比如const、let、箭头函数在较老版本里可能不认识。我上面的代码全部用的是var和传统function写法兼容性是经过验证的。4. 实战踩坑合并过程中最容易翻车的细节4.1 表头处理不当导致数据错位我在帮同事跑合并的时候遇到的第一个问题就是表头错位。她的文件里有的表叫“姓名”有的表叫“员工姓名”内容其实一模一样。按位置合并后新表的第一行表头来自第一个文件后续文件的数据也是按位置堆的所以只要列的物理顺序一致就不会有问题。问题出在另一种情况某个文件的第一列是序号但另一个文件第一列是姓名两列内容完全不一样。这种怎么处理很遗憾脚本没法根据表头名称自动对齐列。我自己遇到这种情况会在跑合并之前先做一个预处理脚本把所有文件统一成相同的列顺序。本质上跨工作簿的“按位置合并”要求各文件结构一致这是所有合并工具绕不开的前提。如果你非要实现“按表头名称匹配合并列顺序不同也能自动对齐”那需要写的代码量会增加三四倍先读取第一个文件的表头再读取每个文件的表头建立列名映射表然后按映射关系写入。如果你确实需要这个能力一个折中的办法是既然表头都是文本你可以在每个文件里用一段预处理宏先把列顺序排成统一标准再加到正式合并流程里。4.2 格式残留让UsedRange虚胖这是Excel自动化里最经典的坑不只在WPS JS宏里有VBA里也有。现象是明明文件只有两行数据但UsedRange的Rows.Count却返回几千行合并出来一大堆空行。原因是源文件曾经编辑过几千行后来删除了内容但Excel的“已用区域”仍然记着那个范围导致程序以为里面还有数据。我的脚本里已经做了整行空值判断会把完全空白的行过滤掉。但如果空行里恰好有一个空格、一个不可见字符哪怕是打印控制符isEmpty就会判断为false空行就被保留了。排查方法是在出错之后打开源文件按CtrlEnd看看单元格会跳到哪里。如果跳到一个很远的地方说明这个文件确实存在格式残留。清理方法是选中那些多余行列右键删除然后保存。对于程序化处理可以做一个自动裁剪有效数据区域的方法从最后一行开始向上找第一个非空行把它作为数据终点。// 计算真实数据范围从UsedRange底部向上找最后一个非空行 var lastRealRow 1; for (var r used.Rows.Count; r 1; r--) { var hasValue false; for (var c 1; c used.Columns.Count; c) { if (used.Cells(r, c).Value2 ! null used.Cells(r, c).Value2 ! ) { hasValue true; break; } } if (hasValue) { lastRealRow r; break; } }把这个逻辑加进去替代直接使用UsedRange的结束行号可以有效避免格式残留导致的“幽灵行”。4.3 日期数字被打乱合并后对不上账合并后日期列变成一串数字是另一个高频问题。比如源文件里写着“2024-03-15”合并后变成了数字看起来完全对不上。原因是Value2拿到的是单元格底层的序列号数值而不是格式化后的显示文本。日期在Excel底层本来就存成一个整数从系统起始日到现在的天数。如果你需要的是“看起来一样的日期字符串”有两个解法。解法一是用Cells对象的Text属性它返回的是单元格显示时的文本。缺点是Text属性依赖当前的单元格格式如果源文件里日期格式不统一得到的文本格式也不统一。解法二是统一格式化在读取的时候判断单元格内容是日期类型就手动转成字符串。var cellValue used.Cells(r, c).Value2; if (cellValue instanceof Date) { cellValue cellValue.getFullYear() - (0 (cellValue.getMonth() 1)).slice(-2) - (0 cellValue.getDate()).slice(-2); }这个方案把日期输出成固定格式不管源文件怎么显示最终汇总表里格式一致后续做数据透视表也不容易出问题。代价是日期将变成文本不能直接参与日期函数计算。如果后续还需要日期计算建议保留数值格式我遇到过不少用户选了文本方案后后面想用月份汇总却算不出来又得返工。4.4 文件多、数据大时的卡死与优化文件数量少的时候脚本跑起来没感觉。一旦文件数量超过二十个、单个文件上万行逐单元格写入的方式速度会明显下降甚至出现“卡死”的假象。这背后的原因很简单每写一个单元格程序都要跟表格对象做一次通信开销很大。一个一万行的文件二十个文件就是二十万次单元格写入每次写入都是一次对象调用不变慢才怪。优化的思路是批量写入把数据先攒到一个二维数组里一次赋值给一个Range区域。但这里有个JS宏的陷阱Range.Value2期待的是一个二维数组而JS里二维数组的行列方向在转换时会容易反转赋值之前不注意就会把行列搞反。我稳扎稳打的方案是用“逐行读取、整行赋值”。每次给一整行的Range赋值这样通信次数从“每个单元格一次”降到“每行一次”速度提升非常明显而且不用担心二维数组行列反转。如果你要进一步提升速度可以先读全部数据到一个数组再用一个Range区域整块写回但需要小心旋转数组这部分代码调试起来费时间日常办公场景逐行赋值已经完全够用。4.5 常见问题速查表问题现象可能原因排查与解决点击运行没反应JS环境没切换看开发工具里是否显示“切换到JS环境”如果不是先切换合并后全是空行整数列判断异常或格式残留直接在源文件按CtrlEnd查看有效区域清理多余行列日期变成数字取了Value2序列号而不是格式化文本判断Date类型后手动格式化或改用显示值列对不上数据串位各文件列顺序不一致先统一所有文件的列顺序再跑合并提示“目录不存在”路径写错或目录不存在检查正斜杠、目录尾部的斜杠是否漏写跑一半报错某个文件损坏或加密用临时脚本单独打开该文件看具体错误信息合并后的公式是静态值读取的是计算后结果这是设计行为需要保留公式需单独处理源文件被莫名修改关闭时保存了确保wb.Close(false)关闭即不保存5. 扩展玩法从“能合并”到“更好用”5.1 把目录路径做成配置单元格免改代码很多同事拿到脚本第一个反应是我不会改代码每次路径变了怎么办。最笨的办法是每次帮你改更好的办法是把路径作为参数放进表格里。我常用的做法是在汇总表里留一个配置SheetA1单元格填源文件夹路径A2单元格填输出文件名。脚本运行第一步先读这两个单元格这样用户只需要改表格内容完全不需要碰代码。var cfgSheet outBook.Sheets.Item(2); // 第二个Sheet当配置页 var folderPath cfgSheet.Cells(1, 1).Value2; var outputName cfgSheet.Cells(2, 1).Value2;这种“配置外置”的思路对非技术同事非常友好。它把脚本包装成了一个工具使用门槛一下子降低了很多。你甚至可以把这个思路用在其他脚本上凡是涉及路径、文件名、关键参数都优先考虑放单元格而不是写死在代码里。5.2 按指定工作表名称合并默认逻辑是取每个文件的第一个Sheet。实际场景中有些文件的数据不在第一个Sheet而在一张叫“数据明细”的表里。这个需求改动量不大加一个配置项遍历时先尝试获取指定名称的Sheetvar sheetName 数据明细; // 配置项 var ws null; try { ws wb.Sheets.Item(sheetName); } catch (e) { ws wb.Sheets.Item(1); // 找不到就用第一个 }注意Sheet不存在时直接访问会抛出异常。用try catch兜底比先判断存在更省事因为判断Sheet是否存在的代码同样要遍历所有Sheet。5.3 自动追加来源文件名方便溯源合并完之后经常有同事问“这行数据是从哪个文件来的”。与其事后查不如在合并时顺手把文件名列加进去。在遍历循环里找出当前文件文件名写入到汇总表数据行的最后一列var fsoName fileList[i].split(/).pop(); // 从路径里截取文件名 // 在写入每行数据时额外写一列 outSheet.Cells(rowOffset, maxCols 1).Value2 fsoName;这里maxCols 1是在原有数据列之后追加一列。每次循环开始前先计算好新列的位置这样不同列数的文件也能对齐。这个功能加进来之后汇总表的可追溯性会强很多数据出了问题很容易定位到源头文件。我个人在实际操作中还喜欢再加上一列“合并时间”记录数据是被哪个批次任务合并进去的。这样即使同一份数据被重复合并多次后续也能区分新旧版本。操作很简单写入一行当前时间字符串几行代码的事但对后续数据审计非常有帮助。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑