ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

VBA实战:多工作簿同名Sheet数据自动汇总指南

2026/10/5 15:45:47 拓冰建站 浏览量
VBA实战:多工作簿同名Sheet数据自动汇总指南 先说个大概率戳中你的场景每个月末各地分公司的报表往同一个文件夹里一扔你得把几十个Excel文件里同名工作表的指定区域挨个打开、复制、粘贴到总表。我之前接过一个类似需求13个分厂、27个工作簿、每个里面都有个叫汇总数据的Sheet数据区从A1到E120。手工操作熟练也需要20来分钟中间接个电话就可能漏掉一个。用VBA做完之后双击运行三秒出结果还带来源文件名标记。这篇就是完整复刻这套方案的干货记录从需求边界拆解、核心代码逻辑到高频翻车点和扩展改造方向都会讲到。适合有一定VBA基础但没系统写过多工作簿汇总的读者也适合纯新手照着参数改完直接跑。1. 场景拆解这个需求真正卡住的点在哪里1.1 看着只有三步实际每一步都有隐藏前提把多个工作簿中同名工作表的指定区域汇总到一起这句话拆开就是三个动作遍历文件夹里的所有Excel文件、在每一个文件里定位到指定名称的工作表、把指定区域的单元格数据搬到汇总表。听起来简单但每一步都有前提条件需要先想清楚。先说文件遍历。文件夹里可能混着.xlsx、.xls甚至.xlsmWPS环境下还可能是.et。用Dir函数能按通配符匹配但如果你只匹配*.xlsx碰到一个.xls文件就悄悄漏掉了而且大部分情况下不会有任何报错提示等你发现总数对不上时已经晚了。更麻烦的是汇总表本身如果也放在这个文件夹里它也会被当成数据源打开——一份数据被自己汇总自己结果完全不可控。再说工作表定位。很多新手写的代码是用Workbooks.Open打开文件后直接wb.Worksheets(1)取第一个Sheet。这种写法在恰好所有源文件第一个Sheet就是要汇总的表时能跑通但只要有一个文件新建了临时Sheet放在前面或者同事把表的顺序调整过汇总结果就会错位。更稳妥的思路是用工作表名称来定位这是这个需求中我认为最重要的设计决策之一。最后是数据提取。如果没有性能意识最常见的做法是打开文件后Range(A1:E120).Copy然后到汇总表Range(A1).PasteSpecial。在文件少、数据量小的时候感觉不出来但我要说一个实测数据27个工作簿、每个120行数据用复制粘贴方式实测耗时接近40秒改用数组批量读写后耗时在2到3秒之间。这个差距不是Excel本身的差异而是剪贴板和界面重绘的开销。从原理上说每次复制粘贴都会触发剪贴板交互、目标区域重算和界面刷新而Range.Value赋值是把整个区域读入内存数组后再一次性写回目标区域交互次数从单元格数量级降到了1次性能自然完全不同。1.2 动手写码之前先把这六个问题问清楚我见过太多人一上来就写代码写到一半发现需求理解错了又推翻重来。与其这样不如在开工前花10分钟把需求边界确认清楚。根据我的经验下面这六个问题决定了代码的复杂度答案不同代码结构天差地别。第一个问题要汇总的工作表在所有源文件中是否一定存在有些分公司可能这个月没交报表或者文件里压根没有这个Sheet。代码应该在找不到指定工作表时跳过该文件并记录日志而不是直接报错中断整个流程。第二个问题指定区域是固定的还是动态的小工厂的报表可能永远都是A1:E120但大部分实际场景中每个文件的数据行数不一样。如果是动态区域就要用Cells(Rows.Count, 1).End(xlUp).Row这种方式找到最后一行或者直接用UsedRange。动态区域虽然代码复杂一些但更符合真实场景。第三个问题要不要表头如果每个源文件都带表头汇总结果通常只需要保留一份表头从第二个文件开始就只追加数据区跳过第一行。这个看似简单的逻辑代码里要专门加一个判断。第四个问题汇总到一个工作表还是按名称拆成多个工作表有时候需求是每个Sheet名对应一个汇总Sheet这个问题在需求确认阶段就要明确后续代码结构差异很大。第五个问题重复运行怎么处理汇总表里已经有上一次的数据了再次运行是直接覆盖还是不清空继续追加我的建议是默认清空指定起始区域后重新写入保证每次运行结果都是干净的全量汇总避免跑了两遍数据翻倍这种低级事故。第六个问题要不要加来源标记出现数据对不上时知道每一行是从哪个文件来的会省很多查证时间。这个功能在代码里只多三行但价值极大。这六个问题每个分支都会在后面代码部分体现出来。你把答案写下来之后再去看第三部分的完整代码会非常清楚每一行在解决哪个问题。2. 核心设计文件遍历、同名Sheet定位与数据写入2.1 文件定位用Dir而不是字符串拼接遍历结束条件别写错老版本Excel自带的Application.FileSearch在Office 2010之后基本就不能用了所以现在文件遍历的标准方案是Dir函数。很多人一开始会写成字符串拼接——把文件夹路径和文件名拼在一起再传给Workbooks.Open但这样有个问题如果文件路径不对报错信息含糊排查成本高。Dir函数的正确用法是Dim filePath As String filePath Dir(folderPath *.xlsx) Do While filePath 这里是处理每个文件的逻辑 filePath Dir 调用不带参数的Dir继续获取下一个文件 Loop四两拨千斤的细节在于第一次调用Dir(folderPath *.xlsx)时传路径和通配符后面循环体内的Dir不能带参数否则会重新开始匹配第一个文件造成死循环。如果你在测试时发现程序一直处理同一个文件九成是这里写错了。为什么用Dir而不是FileSystemObject两个都能用但Dir不需要引用Microsoft Scripting Runtime方便在不同电脑之间分发。不过Dir有个小限制它对文件名字符串的匹配有时不够直观比如要同时匹配.xlsx和.xls就得做成两次遍历混在一个循环里会非常啰嗦。我一般会按需求确定好目标后缀宁可写两次循环也别用模糊匹配把不可预料的文件类型卷进来。关于路径的分隔符VBA里正常用反斜杠结尾的文件夹路径没问题但总有人会把路径写错。我建议在代码里做一次保护性拼接If Right(folderPath, 1) \ Then folderPath folderPath \这样用户在Excel弹窗中选择了文件夹路径后无论有没有末尾反斜杠都能稳定工作。2.2 工作表定位遍历集合按名称匹配而不是按序号拿表文件打开之后下一个任务就是找到名字叫某个指定名称的工作表。最安全的写法是遍历Worksheets集合Dim ws As Worksheet Set ws Nothing For Each ws In wb.Worksheets If ws.Name sheetName Then Set targetWs ws Exit For End If Next ws遍历完如果targetWs还是Nothing说明源文件里没有这个Sheet按之前确认的策略跳过并记录日志即可。为什么要这样大费周章而不是直接Set ws wb.Worksheets(sheetName)其实后者在绝大多数情况下也能用而且代码更短。真正的差异在异常场景下直接按名称索引当名称不存在时会抛出一个运行时错误如果你用On Error Resume Next吞掉错误又容易出现错误处理范围失控的问题——错误被吞掉之后后面所有对象引用出错都不报排错变成噩梦。而遍历集合按名称匹配的方式不依赖错误处理不存在就返回Nothing逻辑上天然安全。另外强调一个新手极容易踩的坑千万不要用wb.Sheets(1)或者wb.Worksheets(1)这种方式定位。Sheet的序号在源文件里是不可控的就像你不能指望所有同事都按同一顺序整理工作簿中的Sheet任何一个文件里多一个Sheet就会错位。按名称定位是唯一正确的做法。2.3 区域数据读取数组批量读写别再用循环单元格我见过不少项目组写的汇总代码是双层For循环遍历单元格取值再写到汇总表文件少时没问题文件一多速度感人。Excel VBA操作单元格的代价远超很多人想象每次单元格读写都涉及COM层交互几百个单元格可能感觉不出来但如果一个文件有几千行、几十个文件叠加慢到怀疑人生。正解是把整个区域一次性读入Variant数组再一次性写回Dim arr As Variant arr sourceWs.Range(srcRange).Value 处理完arr之后一次性写回目标区域 destWs.Range(destCell).Resize(UBound(arr, 1), UBound(arr, 2)).Value arr这个思路用生活化的方式理解就是单元格循环像快递员挨家挨户敲门送件数组批量读写则是先用卡车把一车货拉到小区门口的驿站再统一分发。两者干的是同一件事但效率完全不是一个量级。代码里有个细节容易被忽略Resize的第二个参数UBound(arr, 2)一定要写否则当源区域恰好只有一列时UBound(arr, 2)会报错。单人单列的数据场景比较少见但如果源区域是A1:A10这种单列区域.Value返回的是一个二维数组但第二维上界仍然是1所以正常写UBound(arr, 2)其实没问题反而是在区域只有单个单元格时.Value返回的不是数组需要单独用If判断。稳妥起见我习惯在读取区域前先判断一下区域是否多行多列。3. 可直接落地的完整代码与逐段拆解3.1 参数区先行把所有变化集中在最上面十二行完整代码我给出一份已经实际跑过多种数据形态的版本你使用时优先修改的就是最上面的参数区。把所有可变项集中到顶部是我写VBA工具的习惯这样交付给别人之后对方只需要改参数不用动后面的逻辑代码。 参数区 - 使用时只需修改这里 Const folderPath As String C:\月报数据\ 源文件夹末尾记得带\ Const sheetName As String 汇总数据 要读取的工作表名称 Const srcRange As String A1:E120 指定区域根据实际调整 Const destSheetName As String 总汇总 写入结果的工作表名称 Const destStartCell As String A2 结果开始写入的单元格(避开表头) Const hasHeader As Boolean True 源数据是否带表头 Const addSourceMark As Boolean True 是否添加来源文件标记列参数说明说透一点srcRange可以是固定范围也可以写成一个动态范围的地址。如果你需要动态最后一行可以先用代码算出来再往这个常量里填或者干脆把srcRange改成函数。但为了阅读顺畅我在这份代码里用固定区域演示后面第5部分我会讲动态区域的改造方案。destStartCell设为A2是假设第一行已经手动写好了表头。程序每次运行会先清空从第二行开始的旧数据再重新汇总避免重复运行导致的数据堆叠。如果不想清空旧数据或者想去掉表头把hasHeader改一下逻辑即可。3.2 主程序清空旧数据、遍历工作簿、防呆判断Sub MergeWorkbooks() Dim destWs As Worksheet Set destWs ThisWorkbook.Worksheets(destSheetName) 清空旧数据保留第一行表头 Dim lastRow As Long lastRow destWs.Cells(destWs.Rows.Count, 1).End(xlUp).Row If lastRow 2 Then destWs.Range(destWs.Cells(2, 1), destWs.Cells(lastRow, destWs.Columns.Count)).ClearContents End If 关闭界面刷新和自动计算减少窗口闪烁和大幅提升速度 Application.ScreenUpdating False Application.Calculation xlCalculationManual Application.DisplayAlerts False Dim rowOffset As Long rowOffset 0 Dim currentRow As Long currentRow destWs.Range(destStartCell).Row 遍历源文件夹中的所有xlsx文件 Dim filePath As String filePath Dir(folderPath *.xlsx) Dim wb As Workbook Dim sourceWs As Worksheet Dim srcData As Variant Do While filePath 跳过汇总工作簿自身防止自己汇总自己 If filePath ThisWorkbook.Name Then 以只读方式打开避免锁定用户文件 Set wb Workbooks.Open(folderPath filePath, ReadOnly:True, UpdateLinks:0) 检查是否存在指定名称的工作表 Set sourceWs Nothing Dim ws As Worksheet For Each ws In wb.Worksheets If ws.Name sheetName Then Set sourceWs ws Exit For End If Next ws If Not sourceWs Is Nothing Then 读取指定区域到数组 srcData sourceWs.Range(srcRange).Value 计算有效数据行数根据hasHeader决定是否跳过第一行 Dim startSrcRow As Long startSrcRow 1 If hasHeader Then startSrcRow 2 Dim dataRows As Long dataRows UBound(srcData, 1) - startSrcRow 1 If dataRows 0 Then 一次性写入数组到目标区域 destWs.Range(destWs.Cells(currentRow rowOffset, 1), _ destWs.Cells(currentRow rowOffset dataRows - 1, UBound(srcData, 2))).Value _ Application.WorksheetFunction.Index(srcData, _ Evaluate(ROW( startSrcRow : UBound(srcData, 1) )), _ Evaluate(COLUMN( 1 : UBound(srcData, 2) ))) 如果启用来源标记在最后一列写入文件名 If addSourceMark Then Dim markCol As Long markCol UBound(srcData, 2) 1 destWs.Range(destWs.Cells(currentRow rowOffset, markCol), _ destWs.Cells(currentRow rowOffset dataRows - 1, markCol)).Value filePath End If rowOffset rowOffset dataRows End If Else Debug.Print 跳过(无该工作表): filePath End If wb.Close SaveChanges:False Set wb Nothing End If filePath Dir 不带参数获取下一个文件 Loop 恢复设置 Application.ScreenUpdating True Application.Calculation xlCalculationAutomatic Application.DisplayAlerts True MsgBox 汇总完成共处理数据行数 rowOffset End Sub这段代码里有两处比较绕我用注释和文字分别说清楚。第一处是数据写入方式。这里使用WorksheetFunction.Index配合Evaluate(ROW(...))从数组中截取从第二行开始的子数组一次性写回目标区域。这个技巧绕开了For循环按行写入读取和写入都是批量操作。有人会问为什么不直接srcData整体写过去再加个偏移因为数组中如果包含表头直接完全写过去会把多份表头都带上而我们通常只需要一份表头。用Index函数从数组的第二行开始抽取相当于数组切片干净利落。第二处是Debug.Print。我在目标工作表中找不到指定名称Sheet的情况里输出了被跳过的文件名到立即窗口快捷键CtrlG查看这样你就知道哪些文件有问题需要单独处理。批量跑完看一眼立即窗口比一条条对数量高效太多。3.3 为什么关闭这三个开关以及恢复顺序不能乱代码里我关了三个东西ScreenUpdating界面刷新、Calculation自动计算、DisplayAlerts系统弹窗。这三个是VBA批量处理性能三板斧但每个都有讲究。ScreenUpdating不关的话每打开一个工作簿屏幕都会闪烁切换一下27个文件就闪27次。关掉之后整个操作在后台跑完最后直接出结果。不只是好看性能上每少一次重绘就省一次CPU和显卡的开销。Calculation如果本身工作簿里没有复杂公式这个开关感受不强但汇总表如果有几百行VLOOKUP、SUMIF之类的公式每次写入一片数据都会触发大量重算。关闭自动计算后写入效率能提升数倍。关键点是执行完毕后一定要恢复xlCalculationAutomatic我见过不止一次有人在代码里关了计算但忘记恢复导致用户在手动输入数据时公式死活不动一个个单元格按F9排查了半天才发现是这个开关没复位。DisplayAlerts是在关闭工作簿时防止弹出是否保存更改之类的询问框。这里有个习惯问题我用的是wb.Close SaveChanges:False已经明确不保存了理论上不会弹但打开文件时如果遇到外部链接更新提示关闭时也可能会弹其他警告干脆统一关掉。恢复顺序和关闭顺序没有绝对讲究但建议把ScreenUpdating放最后恢复这样前面恢复计算和弹窗的过程也不会被用户看到。4. 高频翻车点从路径、表结构到性能的完整排查链路4.1 自己汇总自己Dir遍历到结果文件时的死循环隐患这是一个非常隐蔽的坑。我第一次给公司做汇总工具时把汇总表总汇总.xlsx也放在了源文件夹里。Dir遍历时它会被当成一个普通源文件打开然后尝试在里面找汇总数据这个Sheet——它本身是汇总表当然没有这个Sheet所以被跳过了。如果你的汇总表恰好也有同名Sheet或者你设置的srcRange恰好指向它自己那数据就会把自己也汇总进去数字翻一倍。排查这类问题的链路是先看循环执行了filePath的次数是否比预期多了一次或者看结果行数比源文件所有行数之和多了几百行。修复方法是写出明确豁免循环体内比较filePath ThisWorkbook.Name源文件夹里除了数据和汇总表不应该有别的文件。如果以后你要把源文件和汇总文件混在一个文件夹里又嫌手动排除麻烦可以直接修改Dir的匹配模式把汇总文件排除在外但我个人推荐从源头上规划目录结构把数据放一个文件夹、汇总结果放另一个文件夹。4.2 区域尺寸不一致和单列/单单元格区域的处理如果每个文件的指定区域行数不是固定120行比如有的是125行又用了固定区域A1:E120结果就是部分数据被截断或者写入Empty值。这个在公式场景尤其隐蔽因为Range(A1:E120).Value读取超出实际内容的区域时多出来的格子会返回Empty写入汇总表后表现为大量0值或者空白行。排查链路是打开一个源文件按CtrlEnd看Excel实际使用的最后单元格再和代码里写的srcRange比对。如果是动态区域我的常见改法是把srcRange改成函数Function GetDynamicRange(ws As Worksheet) As String Dim lastR As Long Dim lastC As Long lastR ws.Cells(ws.Rows.Count, 1).End(xlUp).Row lastC ws.Cells(lastR, ws.Columns.Count).End(xlToLeft).Column GetDynamicRange ws.Cells(1, 1).Resize(lastR, lastC).Address End Function然后在主程序里把sourceWs.Range(srcRange)替换成sourceWs.Range(GetDynamicRange(sourceWs))即可。注意最后一个单元格的查找方式如果数据中间有空行用End(xlUp)会找到第一个非空位置可能不是真正的最后一行。还有一个我踩过的极端场景源区域是单列A1:A10srcData Range.Value返回的仍是一个二维数组但它的第二维上界是1这时你如果直接UBound(srcData, 2)不会出问题如果区域是单个单元格A1.Value返回的不是数组而是一个标量值此时需要单独处理。我的建议是如果确实可能碰到单单元格区域在读取后加一行判断If TypeName(srcData) Variant() Then 正常数组处理 Else 单单元格情况手动包成数组或者单独写入 End If4.3 打开文件卡住不动外部链接、受保护视图和OneDrive同步跑批的时候如果程序卡在一个文件上很久没反应大多数时候不是代码慢而是打开那个文件时有弹窗被DisplayAlerts吞掉了或者Excel进程在等待一个外部链接服务的响应。我曾经遇到过一个源文件引用了另一个已删除的Excel文件作为外部数据源每次打开它Excel都要等待十几秒的链接更新。防御性写法是Workbooks.Open时传UpdateLinks:0这个参数的作用是打开时不去更新外部链接引用直接加载已有缓存数据。如果文件里包含宏且宏里有Workbook_Open事件代码也会被触发。正常情况下这没问题但在自动化脚本里可能引入不可预期的行为。如果文件来源都可信可以临时禁用源文件的宏事件Application.EnableEvents False 在打开和关闭循环外层加上避免每个文件的Open事件被触发 Application.EnableEvents True别忘了在结束后恢复EnableEvents True一旦忘了不只是你这次运行的问题当前Excel进程里之后所有代码的事件触发都会被关闭排查起来非常抓狂。OneDrive或共享网盘目录里的文件是另一个大坑。如果文件正在被云端同步占用Workbooks.Open返回的可能是缓存副本或者干脆报文件被占用。解决办法是尽量把源文件夹放在本地非同步目录或者在代码前先做一次文件是否可写的探测。4.4 WPS环境下的兼容性差异与未安装VBA支持库问题热词里出现了wps vba、未安装vba支持库、wps vba宏插件下载这几个词说明你现在可能也在WPS里跑这套代码。先说明一个事实WPS个人版默认不带VBA支持库需要单独装VBA for WPS插件才能跑VBA宏。如果没有装打开有宏的xlsm文件时会提示未安装VBA支持库或无法运行文档中的宏。就算装了VBA插件WPS对Excel VBA对象模型的实现也存在少量差异。在WPS中Dir函数、Workbooks.Open的参数、WorksheetFunction.Index这些基础操作和Excel基本一致但一些UI对象、Application.Calculation枚举的处理上偶尔有微差。我在WPS环境实际测试过上述代码主流程可以稳定运行但如果你的文件是.et格式开头说的*.xlsx通配符就匹配不到了需要改成*.et或写两次循环。如果你要在多个同事的电脑上分发最稳妥的做法是在代码最前面检测当前应用类型比如Application.Name不是Microsoft Excel时就弹出一个提示。这比让用户在报错中猜原因省事多了。5. 后续改造方向从能汇总到想怎么汇总就怎么汇总5.1 按指定列合并而不是一次性搬整个区域有时候源表不是专门的报表而是包含了很多无关列。比如一张完整的销售明细表有客户名称、商品、数量、单价、金额、备注、销售人员等十几列但汇总只需要客户、商品、金额三列。一种粗暴做法是设置srcRange为对应列的区域但前提是列位置在所有文件中一致。更稳妥的进阶做法是使用VBA Dictionary热词里的vba字典先读取源表的第一行作为表头再把需要的列名映射成列号最后按列号提取数据。Dim needCols As Variant needCols Array(客户, 商品, 金额) Dim colMap As Object Set colMap CreateObject(Scripting.Dictionary) Dim headerArr As Variant headerArr sourceWs.Range(sourceWs.Cells(1, 1), sourceWs.Cells(1, lastCol)).Value For j 1 To UBound(headerArr, 2) If Not IsError(Application.Match(headerArr(1, j), needCols, 0)) Then colMap(headerArr(1, j)) j End If Next j这样即使源文件的列顺序调整了只要表头名称不变汇总逻辑就能正确匹配。字典在这里的另一个优势是去重判重如果某个文件里同一列名出现两次字典写入时会区分不开但正常表头不会重复所以影响不大。5.2 一个工作簿要汇总多个Sheet而不是一个如果需求变成把每个工作簿里的全部Sheet都汇总到一个总表或者汇总其中指定的几个Sheet核心思路就是双层循环外层遍历文件内层遍历Sheet集合。内层直接复用For Each ws In wb.Worksheets然后对每个Sheet分别调用一次读取数组 写入汇总表的逻辑。注意汇总多个Sheet时必须给目标表增加一个Sheet来源列否则同一文件多份数据混在一起后面完全无法区分。判断哪些Sheet需要汇总可以在内层循环里用Like通配符匹配Sheet名比如If ws.Name Like *汇总* Then。这样新入职同事如果在源文件里新建了一个汇总V2也没关系只要你通配符写得够准还是能抓到。但反过来也要注意通配符过宽导致把无关Sheet也拉进来这时候配合日志输出去检查实际汇总了哪些Sheet很有必要。5.3 来源标记、日志输出和定时自动化的进阶组合基础版的汇总已经能解决大部分问题但交付给别人用时加三个小功能体验会有质的提升。来源标记列写在主流程里已经做了就是在数据区右边多写一列filePath。这样汇总表里每一行都能追溯到来源文件对账时一目了然。日志输出的考虑是大批量运行几十个文件时哪怕只有一个文件缺Sheet混合在结果里也难发现。你现在改代码时输出到Debug.Print立即窗口但普通用户根本不会看立即窗口更实用的做法是把日志写到汇总表右侧的隐藏列或者直接写一个文本日志文件到指定路径。每次运行结束之后程序给出总数XX个文件成功XX个失败XX个这样一句汇总有异常文件再单独列出来实用性远高于一个简单的汇总完成。定时自动化方案有两种方向。方向一是在代码里用Application.OnTime设定特定时间自动执行汇总宏方向二是用Windows任务计划程序定时打开Excel并运行宏。这两种都属于把一个手工VBA工具变成无人值守后台任务的思路。我不建议一上来就做自动化先把手动版跑两周稳定了再上计划任务不迟。而且自动化之后要做到失败了会通知人比如说一个异常弹窗或者写日志文件不然你都不知道汇总没跑成功。写在最后的几个操作心得实际跑这个工具的过程中我有一个很深的体会先在两三个文件的小目录下跑通逻辑再放全量文件。小目录测试时打开立即窗口看日志确认被处理的文件和你预期一致再上全量。直接上全量然后发现结果不对排查成本会高很多。另外Application.Timer这个函数是个低调但好用的调试工具。在代码开头记录Dim t As Double: t Timer结束后用Debug.Print 耗时 Timer - t 秒每次改动代码之后对比耗时能直观感受到哪些操作拖慢性能。别相信感觉数据说话。最后就是老生常谈但必须提的任何汇总工具批量覆盖目标区域之前先手工备份一份汇总表。代码里清空旧数据的逻辑越完整越要确认文件路径别选错。我的习惯是在运行前按F5之前先把文件另存为带日期后缀的版本跑完确认没问题再清理。这套习惯不复杂但能在你某天头脑发热把参数改错的时候帮你保住几个小时的重复劳动成果。希望这份完整记录能帮你一次就把多工作簿汇总这件事做顺手。