1. 问题背景:为什么要批量合并同名工作表
本文主题是 批量合并多个工作簿中的同名工作表。这个场景在真实办公里很常见:多个部门、多个分部、多个项目分别提交 Excel 文件,每个文件里都有一张同名工作表,最后需要汇总成一份总表。
比如华北、华东、华南三个分部各自提交一个 上半年.xlsx 文件,每个文件里都有一张叫 统计 的工作表。现在领导不想看三个文件,只想看一份 上半年汇总表.xlsx。如果手工处理,就要不断打开文件、复制数据、粘贴到总表,这就是非常典型的“复制粘贴型加班”。
这张图展示的是本文的整体目标:把多个工作簿中的同名工作表,批量合并到一个新的汇总工作簿中。

从图中可以看出,本文的重点不是处理单个 Excel,而是把多个来源文件的数据统一汇入一个结果文件。这类任务的核心不是“会不会复制”,而是能不能稳定地批量读取、累积数据、生成汇总结果。
这里最容易翻车的地方,是把每个工作簿的表头都一起复制进去。如果每合并一个文件就追加一次表头,最后的汇总表里会反复出现“日期、产品、销量、金额”这样的标题行,后续筛选、排序都会受到影响。
2. 应用场景:多个分部统计表合成一份汇总表
批量合并同名工作表,本质上解决的是“分散数据集中化”的问题。只要多个 Excel 文件结构相似,且目标工作表名称一致,就可以考虑用脚本自动合并。
这张图展示的是典型应用场景:多个分部统计表,最终汇总成一份总表。

从图中可以看到,这类需求常见于财务汇总、销售汇总、库存汇总等场景。每个分部都有自己的统计表,但管理层真正需要的是一份统一结果。Python 在这里的价值,就是把重复复制粘贴变成稳定规则。
我更建议把这类任务理解成一个标准流程:先确定文件夹,再逐个打开工作簿,找到指定同名工作表,读取数据,持续累积,最后统一写入一个新的汇总工作簿。
这张流程图里最重要的是两个判断:第一,目标工作表是否存在;第二,当前是不是第一次读取数据。前者决定文件要不要跳过,后者决定是否要读取表头。
3. 核心原理:表头只取一次,数据持续累积
批量合并多个工作簿时,最关键的逻辑不是打开文件,而是处理表头和数据的关系。正常情况下,每个工作簿里的同名工作表都有一行表头,但最终汇总表只需要一份表头。
这张图展示的是本案例的核心逻辑:第一次读取时保留表头,后续工作簿只追加数据行。

从图中可以看出,header = None 表示表头还没有被初始化,all_data = [] 表示先准备一个空列表,用来持续累积后续读取到的数据行。第一次遇到有效表格时,取出表头;之后每读取一个文件,就只把数据部分追加到 all_data 中。
header = None 的语义是“还没有拿到表头”,而不是“表头为空”。这个区别很重要。None 更像一个初始化标记,表示后面第一次读取到表格时,需要把第一行保存为表头。
header = None
all_data = []
if header is None:
header = table[0]
values = table[1:]
all_data = all_data + values
这里的 table[0] 表示第一行,也就是表头;table[1:] 表示从第二行开始的所有数据行。最终写入汇总表时,用 [header] + all_data 拼成完整内容。
如果不区分表头和数据,汇总表后期会很难用。重复表头会影响筛选、排序、公式引用,也会让数据看起来不干净。这种问题不是语法错误,但属于典型的数据质量问题。
4. 实现流程:遍历文件、读取同名表、写入汇总簿
在看完整代码之前,先把流程想清楚。这个案例可以拆成八个动作:遍历文件夹、跳过临时文件、检查是否存在目标工作表、读取表格数据、累积到 all_data、新建汇总工作簿、写入表头和数据、最后执行 autofit() 自动调整显示效果。
这张图展示的是完整实现流程,从文件遍历一直到最终生成汇总表。

从图中可以看出,真正稳定的脚本不是“打开文件然后复制”,而是每一步都有判断。比如遇到 ~$ 临时文件要跳过;目标工作表不存在要跳过;数据为空也要跳过。批处理脚本越是面对多个文件,越不能假设每个文件都完全正常。
下面是一份可直接修改使用的完整代码。实际使用时,只需要改三个参数:输入文件夹、目标工作表名称、输出文件路径。
import os
import xlwings as xw
# ====== 需要根据实际情况修改的参数 ======
folder_path = r"e:\file\target" # 多个工作簿所在目录
sheet_name = "统计" # 需要合并的同名工作表
out_file = r"e:\file\上半年汇总表.xlsx" # 输出汇总文件
# ======================================
app = xw.App(visible=False, add_book=False)
header = None
all_data = []
try:
for file in os.listdir(folder_path):
# 1. 跳过 Excel 临时文件
if file.startswith("~$"):
continue
# 2. 只处理 Excel 文件
if not file.lower().endswith((".xlsx", ".xls", ".xlsm")):
continue
full_path = os.path.join(folder_path, file)
wb = app.books.open(full_path)
# 3. 判断目标工作表是否存在
sheet_names = [s.name for s in wb.sheets]
if sheet_name not in sheet_names:
wb.close()
print(f"跳过:{file},不存在工作表:{sheet_name}")
continue
sht = wb.sheets[sheet_name]
# 4. 从 A1 开始扩展读取表格区域
table = sht.range("A1").expand("table").value
# 5. 判断是否有有效数据
if not table or len(table) < 2:
wb.close()
print(f"跳过:{file},数据为空或只有表头")
continue
# 6. 第一次读取表头
if header is None:
header = table[0]
# 7. 后续只累积数据行
values = table[1:]
all_data = all_data + values
wb.close()
print(f"已读取:{file} -> [{sheet_name}]")
# 8. 新建汇总工作簿
out_wb = app.books.add()
out_sht = out_wb.sheets[0]
out_sht.name = "汇总"
if header is None:
out_sht.range("A1").value = [["没有读取到任何有效数据"]]
else:
out_sht.range("A1").value = [header] + all_data
out_sht.autofit()
out_wb.save(out_file)
out_wb.close()
print(f"汇总完成:{out_file}")
finally:
app.quit()
这段代码的骨架很清楚:外层负责遍历文件,内层负责读取指定工作表,最后统一写入汇总工作簿。它不是一次性脚本,而是一个可以复用的批量合并模板。
5. 关键代码拆解:为什么这样写
5.1 为什么要跳过临时文件
Excel 打开文件时,目录里可能出现 ~$ 开头的临时文件。这类文件不是正式工作簿,脚本如果尝试打开它,很容易报错。
if file.startswith("~$"):
continue
推荐所有批量处理 Excel 的脚本都加上这个判断。这不是多余代码,而是稳定性保障。
5.2 为什么要判断同名工作表是否存在
并不是所有文件都一定有目标工作表。比如有些分部没有提交 统计 表,或者表名写成了 数据统计。如果不判断直接读取,就会报错。
sheet_names = [s.name for s in wb.sheets]
if sheet_name not in sheet_names:
wb.close()
print(f"跳过:{file},不存在工作表:{sheet_name}")
continue
不要默认所有文件都符合模板。真实办公里,文件命名、sheet 命名、字段顺序经常会有偏差。脚本必须允许异常文件跳过,而不是因为一个文件异常导致整个任务中断。
5.3 为什么用 expand(“table”)
sht.range("A1").expand("table").value 的意思是从 A1 单元格开始,自动扩展读取连续的数据区域。只要表格从 A1 开始,并且中间没有完全空白行列,这种写法就比较方便。
table = sht.range("A1").expand("table").value
这里有一个前提:表格必须从 A1 附近开始,并且数据区域要连续。如果表格上方有标题说明、空行、合并单元格,expand("table") 可能无法读到你想要的区域。这类文件需要先规范模板,或者改用更明确的读取范围。
5.4 为什么最后要 autofit()
写入 Excel 后,列宽可能不合适,长文本会被遮住,数字也可能显示成 #######。所以写完数据后执行 autofit(),能让汇总表更接近可阅读状态。
out_sht.autofit()
推荐在生成结果文件时做基础美化。办公自动化不是只把数据写进去,还要考虑别人打开文件时能不能直接看。
6. 效果验证:汇总后必须检查这几项
批量合并脚本跑完后,不能只看控制台有没有报错。更稳的做法,是对输出结果做基本校验。
第一,要检查输出文件是否生成;第二,要检查汇总表是否只有一行表头;第三,要检查数据行数是否符合预期;第四,要抽查几条来源数据,看是否确实进入汇总表。
print(f"汇总数据行数:{len(all_data)}")
print(f"输出文件:{out_file}")
如果希望日志更清楚,可以记录每个文件的处理状态。比如哪些文件成功读取,哪些文件缺少目标 sheet,哪些文件为空。这样后续复盘更方便。
log_list = []
log_list.append({
"文件名": file,
"目标工作表": sheet_name,
"处理结果": "已读取",
"数据行数": len(values)
})
对于真实办公场景,我更建议把处理日志也导出成 Excel。因为脚本输出的结果文件可能会被别人复查,处理日志就是你的证据链。
7. 常见问题与踩坑提醒
7.1 表头不一致会导致汇总结果变脏
如果不同工作簿里的 统计 表字段不一致,比如有的叫 销售额,有的叫 金额,脚本虽然可能能合并,但最终结果会出现字段含义不统一的问题。
合并前最好先校验表头是否一致。否则看起来合并成功,实际数据口径可能已经错了。
if header is not None and table[0] != header:
print(f"表头不一致:{file}")
7.2 空行和合并单元格会影响读取范围
expand("table") 适合连续表格。如果 Excel 里有空行、合并单元格、额外标题、说明文字,读取范围可能不符合预期。
推荐先统一模板,再批量合并。如果模板混乱,脚本只是把混乱放大,不能自动把所有脏数据变干净。
7.3 文件被打开可能导致保存异常
如果某个源文件正在被打开,或者输出汇总文件已经被打开,脚本可能无法保存。尤其是在共享盘、同步盘、企业网盘目录中,这个问题很常见。
建议先把待处理文件复制到本地临时目录,例如 C:\Temp\excel_merge,确认无人占用后再执行脚本。
7.4 xlwings 依赖 Excel 环境
xlwings 的优势是能像人工一样操作 Excel,但它通常依赖本机安装的 Excel。如果电脑没有安装 Excel,或者 Excel 插件、弹窗、权限策略异常,脚本也可能运行失败。
如果只是读取和合并数据,后续也可以考虑用 pandas + openpyxl 做无界面处理。但本文沿用书中思路,重点练习的是 xlwings 操控工作簿和工作表的能力。
8. 举一反三:一个工作簿内合并 1月 到 6月
前面的案例是多个工作簿合并到一个汇总簿。其实同样的思路也可以用于一个工作簿内部,比如一个 年度数据.xlsx 中有 1月、2月、3月、4月、5月、6月 这些工作表,现在要合并成一张 上半年汇总。
这张图展示的是扩展案例:在同一个工作簿中,把 1月 到 6月 的工作表合并到一张新表中。

从图中可以看出,虽然合并范围从“多个文件”变成了“一个文件里的多个 sheet”,但底层逻辑没有变:第一次取表头,后续只取数据,最后写入新的汇总工作表。
import xlwings as xw
file_path = r"e:\file\年度数据.xlsx"
month_sheets = ["1月", "2月", "3月", "4月", "5月", "6月"]
app = xw.App(visible=False, add_book=False)
header = None
all_data = []
try:
wb = app.books.open(file_path)
for name in month_sheets:
sheet_names = [s.name for s in wb.sheets]
if name not in sheet_names:
print(f"跳过:不存在工作表 {name}")
continue
sht = wb.sheets[name]
table = sht.range("A1").expand("table").value
if not table or len(table) < 2:
print(f"跳过:{name} 数据为空或只有表头")
continue
if header is None:
header = table[0]
all_data = all_data + table[1:]
if "上半年汇总" not in [s.name for s in wb.sheets]:
wb.sheets.add("上半年汇总")
sum_sht = wb.sheets["上半年汇总"]
sum_sht.clear()
if header is None:
sum_sht.range("A1").value = [["没有读取到任何有效数据"]]
else:
sum_sht.range("A1").value = [header] + all_data
sum_sht.autofit()
wb.save()
wb.close()
finally:
app.quit()
这个扩展案例非常适合练习“同一逻辑迁移到不同场景”。如果能把多个文件合并、同文件多 sheet 合并都理解清楚,后续处理月报、季度报、分部报表会轻松很多。
9. 总结提升:合并不是复制,合并是规则化处理
这一篇的核心,不是简单记住 all_data = all_data + values,而是理解批量合并背后的自动化思维。多个工作簿中的同名工作表,本质上是多个结构相似的数据源;汇总工作簿,则是统一输出结果。
本文最关键的判断有三点:第一,目标工作表是否存在;第二,是否第一次读取表头;第三,读取到的数据是否有效。只要这三点处理好,脚本的稳定性就会明显提高。
真正可复用的脚本,不是只会在一次练习文件上跑通,而是能处理异常、能跳过无效文件、能输出清晰结果。这才是办公自动化和简单代码练习之间的区别。
最后提醒一句:批量合并前一定要备份源文件,并用少量样本先测试。尤其是涉及财务、库存、资产、人事等数据时,脚本跑得快不是优点,跑得稳、结果可验证才是优点。












