☰
Pandas批量合并Excel:一套可复用的数据汇总脚本
2026/10/14 1:03:41 网站建设 项目流程

做数据分析这行,被问得最多的一个需求就是“能不能帮我把几百个Excel合并成一个”。问这话的人,往往手里攒着几十上百个同结构的报表,正一个个打开、复制、粘贴,弄到晚上九十点才发现贴错了几行。我第一次正儿八经被这个需求缠住,是接手一个“汇总全国门店季度销售明细”的小任务。打开第一个文件时还没觉得什么,打开第二十个的时候我就知道,这活绝对不能靠手点。于是花了一晚上写了套基于Pandas的处理脚本,从此但凡遇到类似“汇总大量Excel”的活儿,都是同一套思路改改路径直接跑。

这篇文章就是把那套思路整理出来,完整讲清楚从摸底文件、统一表头、批量读取、清洗合并到最终导出报表的每一个环节,顺便把我踩过的坑一并交代了。适合两类人看:一是被Excel汇总折磨过、想用Python解放双手的运营和数据分析岗;二是刚学Pandas、手头正好有实际项目想练手的开发者。看完之后,你至少能自己写出一套稳定、可复用的大批量Excel汇总脚本。

1. 为什么非要写代码来做这件事

先说个很实在的问题:Excel本身有合并功能,微软也给了Power Query,为什么还要专门写个Pandas脚本?这个问题我每年都要向同事解释一遍。关键在于“量”和“变”两个字。

1.1 手工合并Excel的三个死穴

第一个死穴是量大。几十个文件个个都是几万行的明细表,光是打开文件、全选、复制、切窗口、粘贴就要重复几百次。人不是机器,重复操作做到第五十遍的时候,脑子基本已经不转了,这时候最容易出问题。

第二个死穴是容易错。手工粘贴最常见的翻车现场有三个:漏贴了某一行、把A表的列贴到B表对应错位、公式在粘贴后变成了一堆错误值。最麻烦的是这类错误往往要等到最后对总数对不上时才发现,一旦发现,就得从头排查是哪个文件哪一列出了问题,那种半夜对着屏幕一行一行找错的感觉,经历过的人都懂。

第三个死穴是重复劳动。月度、季度、年度都要汇总一次,这次汇总完了,下个月又来一批新报表,还是同样的打开、复制、粘贴流程。你会发现自己把同样的时间浪费在了同样的事情上。写脚本的意义不只是这次能做完,而是把“用脚本跑一遍”变成了一项可复用的资产——下个月、下一年、下一个部门来问,都是十分钟内出结果。

1.2 为什么选Pandas而不是VBA或手动合并

有人会问:VBA宏合并Excel不也行吗?确实行,但写过VBA的人都知道,VBA在Excel里运行需要启用宏、不同版本的Office对宏的支持参差不齐,而且一旦文件换了路径或者表头加了一列,宏的修改成本一点都不低。Power Query更适合做一次性整理,但它的清洗逻辑藏在图形界面里,可复现性差,碰到复杂的分组统计和跨文件关联时,操作起来会非常绕。

Pandas的优势是逻辑完全代码化:每一步做了什么、每个字段怎么统一、哪些文件跳过不读,全都写在脚本里,谁拿到都能看懂、都能复现。这就像开车,手工合并是手动挡,每一步都得自己换挡;VBA是半自动挡,省一点力但变速箱逻辑不透明;Pandas是自动挡加导航,你只需要定好目的地,路线和速度都交给它处理。

1.3 整套流程的脉络先记住

做大量Excel汇总,核心脉络永远是四步走,我之后所有内容都是围绕这四步展开的:

  1. 摸底:搞清楚文件放在哪、是什么格式、表头在第几行、要读哪些列、有没有脏文件。
  2. 读取:用Pandas把每个Excel读成DataFrame,并统一结构。
  3. 合并:把所有DataFrame纵向堆叠成一个大表,并加一列“来源文件”方便追溯。
  4. 输出:把大表导出成新的Excel或继续做分组、透视、统计。

别小看第一步,我见过太多人上来就直接写循环去读文件,结果报错报得莫名其妙。先花15分钟摸底,比花2小时调试代码划算得多。

2. 动手前的盘底:环境和文件情况必须搞清楚

写代码是最简单的一步,真正花时间的永远是“搞清你的数据长什么样”。我把这部分放在前面,是因为这里决定了你后面要设置的参数。

2.1 先准备环境,用虚拟环境隔离依赖

先从环境做起。Pandas本身是一个Python库,安装命令是:

pip install pandas openpyxl xlrd

这里有几个细节值得说清楚。如果电脑上同时装了Python 3和Python 2,或者好几个项目混在一起,强烈建议先建一个虚拟环境再装依赖,避免不同项目的库版本互相打架。我自己习惯用python -m venv env建环境,然后激活它再去装库。

第二个细节是openpyxl和xlrd这两个库的用途:openpyxl负责读写新版Excel文件(.xlsx),xlrd负责读取旧版Excel文件(.xls)。注意,纯Pandas读Excel做合并,必须依赖这两个库之一。还有一点特别容易踩坑:xlrd在2.0版本之后只支持.xls格式,如果你装了xlrd 2.x却拿它去读.xlsx文件,会直接报错。所以我的建议是:一律用openpyxl读.xlsx,用xlrd读.xls,不在一个库里死磕所有格式。

2.2 先翻文件,别急着写循环

拿到一个合并需求,我做的第一件事从来不是打开编辑器写代码,而是先到文件夹里把所有文件翻一遍。这步听起来很蠢,但价值极大。

具体翻什么?首先看文件命名规律。比如“华东-1月-上海店.xlsx”“华东-2月-上海店.xlsx”这种命名,后面能做很多自动提取;如果命名乱七八糟,至少你得知道哪些文件是目标文件,哪些是临时文件得排除。

其次看文件结构。选三五个文件打开,确认表头在第几行、有哪些列、列名是否完全一致、有没有合并单元格、有没有“总计”行混在数据里、日期字段的格式是什么。这些信息直接决定你后面read_excel要怎么写参数。我遇到过一个案例:同一批报表,一半是表头在第2行,另一半在第3行,因为有一部分报表上面多了一行打印日期。不摸底直接写代码,第一次跑必然一片报错。

2.3 表头不一致的三种应对方案提前想好

表头不统一是所有Excel合并里最恶心的问题,没有之一。事前摸底能帮你发现大部分不一致,但总有几个“特立独行”的文件会在你意料之外。这时候用下面三种方案配合处理:

方案A:读进来后强制重命名列名。适用于列数相同、位置一致,只是列名写法不同的情况,比如“销售额”“销售金额”“销售总额”。代码上就是读完后执行df.columns = ["日期","门店","销售额"]这种操作。

方案B:读取时用header参数指定表头所在行。比如文件前两行是无关内容,表头在第3行,就写header=2。这样读进来的DataFrame第一行就会变成真正的列名。

方案C:用skiprows跳过前面若干行,再用df.iloc[0:]或pd.read_excel配合header=None手动指定列名。这种最灵活,适合那些表头前内容不固定、或者根本没有表头只有数据的文件。

等到读取阶段,这些方案会实际落地。现在只需要在脑子里有个概念:统一列名不是靠幻想,是得靠代码做出来的。

3. 核心实现:Pandas读取与合并的完整流程

到正餐了。假设你的文件夹长这样:data/目录下有几十个xlsx文件,结构基本一致,你要把它们合并成一个总表,并在总表里标明每行数据来自哪个文件。

3.1 用pathlib和glob把文件清单找出来

读取文件的第一步是“找到所有文件”。我不推荐在代码里写死文件路径,更不推荐用字符串拼接路径,因为Windows路径里的反斜杠分分钟能给你造出难以察觉的坑。用pathlib最稳妥:

from pathlib import Path folder = Path("data") files = list(folder.glob("*.xlsx")) print(f"共找到 {len(files)} 个Excel文件")

这里要注意glob的顺序问题。文件夹里的文件顺序不是固定的,如果合并结果需要按月份排序,你最好在循环前先排序:

files = sorted(folder.glob("*.xlsx"))

Path.glob返回的路径天然就是Path对象,直接传给Pandas没问题。而且Path对象的name属性可以轻松拿到文件名,这给后面加“来源文件”列做了铺垫。

如果还想递归查找子文件夹里的所有Excel,改一行就行:

files = sorted(folder.glob("**/*.xls*"))

**/*.xls*会匹配所有子目录下的.xlsx和.xls文件。注意,*.xls*这种写法会把.xlsx匹配进来,如果只想匹配.xlsx就写*.xlsx,想同时匹配旧格式就写*.xls*,自己按需选择。

3.2 读Excel时这些参数才是主角

找到文件后,就要用pd.read_excel把文件读成DataFrame了。裸调用也能读,但真正干活时必须知道几个关键参数:

df = pd.read_excel( file, sheet_name=0, # 读第1个sheet,也可以写sheet的名字 header=0, # 第1行作为列名,视摸底情况调整 usecols="A:F", # 只读A到F列,节省内存 dtype={"销售额": float} # 指定列的数据类型 )

逐一解释为什么这些参数重要。

sheet_name:文件多sheet的情况很常见。如果每个文件都读同一个sheet,直接用sheet名最直观;如果确定每个文件都只有1个sheet,写0就行;如果有些文件sheet名不一样,需要先摸清。

header:默认0表示用第1行做列名,这也是大多数人最容易翻车的地方——很多Excel的第1行是标题,比如“某公司季度销售汇总”,第2行才是“日期 门店 销售额”,这时候不写header=1,读进来的列名就全乱了。

usecols:数据量大的时候只读需要的列能省一半以上内存。比如只要日期、门店、销售额三列,就写usecols=["日期", "门店", "销售额"]或者usecols="A,B,D"。如果不用这参数,那些你根本用不上的三百列也会全数加载进内存。

dtype:显式指定某些列的数据类型,能阻止Pandas把“销售额”读成文本。这在后面做sum()和mean()的时候会省去大把转换的麻烦。

还有一个容易忽略的点:read_excel引擎。Pandas会根据文件扩展名自动选择引擎,xlsx用openpyxl,xls用xlrd。只要你安装了对应库,通常不用手动指定。

3.3 合并前先清洗,合并时用concat

读取只是第一步,真正工程化的代码至少要包含三件事:统一列名、清洗数据、加来源标记。我直接给一个完整可用的函数式实现,这也是我日常工作里最常用的一段模板:

import pandas as pd from pathlib import Path def merge_excels(folder_path, output_path): folder = Path(folder_path) files = sorted(folder.glob("*.xls*")) all_frames = [] failed_files = [] for file in files: try: df = pd.read_excel( file, sheet_name=0, header=0, usecols=["日期", "门店", "销售额"], ) # 统一列名 df.columns = ["日期", "门店", "销售额"] # 加来源文件标记,方便追溯 df["来源文件"] = file.name # 基础清洗 df["销售额"] = pd.to_numeric(df["销售额"], errors="coerce") df["日期"] = pd.to_datetime(df["日期"], errors="coerce") all_frames.append(df) except Exception as e: failed_files.append((file.name, str(e))) print(f"读取失败: {file.name}, 原因: {e}") continue if not all_frames: print("没有任何文件读取成功,请检查文件格式") return result = pd.concat(all_frames, ignore_index=True) result = result.dropna(subset=["日期", "销售额"]) result.to_excel(output_path, index=False) print(f"成功合并 {len(files) - len(failed_files)} 个文件,共 {len(result)} 行") if failed_files: print("以下文件处理失败:") for name, info in failed_files: print(f" - {name}: {info}") merge_excels("data", "汇总结果.xlsx")

这段代码里有几个细节值得多看两眼。

第一个是try/except。批量处理大量文件时,总有一两个“刺头”文件会让你整个脚本中断:例如某文件其实不是Excel只是改了后缀,或者文件被某个人加了密码,又或者里面某个单元格画满了特殊字符。不用try/except包起来,一个刺头就能让前面所有处理白跑。用try/except把一个文件跳过去,记录下来是谁出的问题,等主流程跑完再去单独处理它,这才是工程化该有的态度。

第二个是pd.to_numeric(..., errors="coerce")。销售额这一列在原始报表里经常带着“¥”符号或者千位分隔符,Pandas读进来就成了"1,200.00"这种文本。errors="coerce"会把转换不了的值变成NaN,后续再用dropna把没有销售额的空行删掉。虽然偶尔会把正常数据也误伤,但比起整个脚本因为某个脏数据崩溃,这个代价完全值得。

第三个是ignore_index=True。合并多个DataFrame后索引默认是保留原文件的索引号,一堆重复的0、1、2交错在一起特别乱,也影响后面按索引筛选。设置ignore_index=True后,Pandas会从0开始重新生成一个连续索引,干净利落。

第四个是最后用dropna(subset=["日期", "销售额"])兜底清洗。它会把日期或销售额为空的整行删掉。这个操作帮我处理掉了很多“Excel里看起来正常但其实是空白行”的脏数据,但也要注意:如果原始数据里销售额本来就允许为空,删之前要三思。

3.4 把结果输出成汇总Excel

合并完的大表要输出,最简单的写法是:

result.to_excel("汇总结果.xlsx", index=False)

index=False一定不能漏。如果漏了,Pandas会把默认索引也写进Excel,你最前面的A列就会多出一列莫名其妙的数字。

如果要把结果拆成多个sheet,用ExcelWriter:

with pd.ExcelWriter("汇总结果.xlsx") as writer: result.to_excel(writer, sheet_name="全部数据", index=False) summary.to_excel(writer, sheet_name="汇总统计", index=False)

还有一个实际工作中的坑:Excel对单个sheet的行数上限是1048576行,如果合并结果接近这个数,要么拆成多个sheet,要么输出成CSV。CSV没有行数限制,读起来也快,就是打开方式不一样。遇到超大数据量的时候,别死磕Excel,果断转CSV配合分片处理。

4. 实战排雷:这些坑卡住了大多数人

代码是能跑通的,但现实世界的数据永远比你想象的更脏。这一节我把自己实际踩过的坑和排查经验整理成速查,每一条都是真金白银换来的教训。

4.1 表头不是第一行、有空标题行、有合并单元格

这是最经典的问题。很多业务报表会在表头上面加两行表头说明,或者把几个表头单元格做了合并。Pandas读进来之后,你可能发现列名是Unnamed: 0、Unnamed: 1这种奇怪的字符。

解决套路分两步。第一步清楚空行:读的时候用header指定表头真正的所在行。第二步,如果表头里真有合并单元格导致的命名混乱,常有“列名是Unnamed但数据本身是正常的”的情况,可以直接读完后手动覆盖列名:

df = pd.read_excel(file, header=2) df.columns = ["日期", "门店", "销售额", "备注"]

别指望Pandas能自动识别合并单元格的语义,人在读取前先肉眼确认列顺序才是正道。合并单元格的值通常只会出现在第一行,后续全是NaN,如果要保留,用ffill()填充即可:

df["门店"] = df["门店"].ffill()

这里要注意ffill()的原理就是把前一个非空的值填充到后面的空值里。它只适合合并单元格这种“上一行就是真实值”的场景,不能乱用——如果某字段正常数据本身就允许为空,这时候ffill()会把不该补的值补上去。

4.2 销售额是文本、日期格式五花八门、数字带符号

这是数据清洗的日常。销售报表里“销售额”列出现1,200.00、¥1200、1200元都不是什么稀罕事,还有日期,有的写2024-01-01,有的写2024/1/1,甚至有的写20240101。

面对这种情况,我的固定套路是:

  • 数字列统一走pd.to_numeric,能转换的就转换,到最后仍转换不了的用errors="coerce"变成NaN;
  • 日期统一走pd.to_datetime,明确知道格式时就指定format参数,比如pd.to_datetime(df["日期"], format="%Y/%m/%d"),这样转换速度会快很多倍;
  • 带“¥”或“元”这类文本符号的,优先用字符串的替换方法df["销售额"] = df["销售额"].str.replace("¥", "")再转数值。

这里面有一个容易被忽视的细节:日期转换不要盲目用errors="coerce",因为它会把一些异常值悄无声息地变成NaN。如果汇总之后发现行数和原来对不上,先怀疑是不是这里误伤了。稳妥的做法是转换后打印出多少行变成了NaN,再决定是删是留:

date_na_count = df["日期"].isna().sum() if date_na_count > 0: print(f"有 {date_na_count} 行日期解析失败")

4.3 几百个Excel一起读内存疯了怎么办

数据量大的时候,把所有Excel全读进内存再合并,轻则卡顿,重则直接内存爆炸。我处理过一个需求,300个Excel文件,每个都有几万行二十几列,直接全部读入时内存直接飙到十几G。

解决思路有三个层级,按需选择:第一,用usecols只读需要的列,这是性价比最高的一招;第二,循环里先把单个DataFrame处理完再做groupby聚合,聚合结果再append进列表,这样内存里永远只留一份汇总后的数据,而不是300份原始数据;第三,如果还是扛不住,分批读取,每读100个文件就把结果写到一个中间CSV,最后再合并中间文件。

从工程角度讲,别迷信一次load all,能用流式多少做一点算一点。而且即使内存够大,数据量太大也会拖慢后面的透视和统计操作,早清洗、早聚合、早瘦身永远是好习惯。

4.4 文件名中文乱码、路径里的反斜杠、非法字符

Windows上的中文文件名和包含空格的路径,是新手最常遇到的坑。直接写在字符串里还好,但它带来的最大风险在于反斜杠转义,比如"D:\data\汇总.xlsx"里的\d和\汇可能被解析成特殊字符。解决方式就是不要手写路径字符串,全程用Path对象:

folder = Path("D:/data") # 或者 folder = Path("data")

Path对象处理路径时用的是正斜杠逻辑,不用管反斜杠转义问题,而且跨平台通用。中文字符在Path里也没问题,Python 3默认用utf-8。

还有一个细节:当你要把“来源文件”列写入Excel的时候,如果文件名里带了/或\,Excel会提示“不能使用这些字符”,遇到就要先替换:

df["来源文件"] = df["来源文件"].str.replace(r"[\\/]", "_")

4.5 常见问题排查速查表

把这些年遇到的高频报错和现象整理成一张表,定位问题的时候可以照着查。

现象可能原因解决思路
FileNotFoundError路径写错或反斜杠转义问题改用Path对象,确认路径层级
读取出来只有一列文件实际是CSV或用其他分隔符用pd.read_csv或指定sep读取
列名全是Unnamed表头不在第一行或存在合并单元格调整header参数,手动覆盖列名
销售额求和失败列数据是文本含符号pd.to_numeric配合str.replace
日期无法排序日期格式混杂pd.to_datetime统一格式
部分文件读不了加密文件、伪装后缀、损坏文件try/except跳过并记录文件名
合并后行数对不上errors="coerce"误伤数据检查NaN数量,溯源脏数据
写入Excel报错“sheet名超过31字符”sheet_name太长用截断name[:31]

4.6 不要忽略“空文件”和“空表”

还有一种很隐蔽的坑:某个文件名看起来正常,里面却没有任何数据,只有几个合并单元格或者一行标题。read_excel读到这种文件返回的是一个几乎全NaN的DataFrame。如果不做过滤,合并结果里会混进一堆空行,统计总数时对不上账。

稳妥做法是在读取后立刻检查:

if df.empty or len(df) == 0: print(f"跳过空文件: {file.name}") continue

甚至更进一步,检查真正关心的列有没有有效值,比如df["销售额"].dropna().empty时视为无数据文件。别怕多写这几行,它们能救你于水火之中。

5. 合并之后的常用加工:让结果真正可用

很多教程到合并就结束了,但实际业务里,合并只是前菜。你还得让这一大坨数据变得可读、可分析、可汇报。分享几个我每次合并后必做的加工。

5.1 从文件名里提取信息,做透视基础

文件名往往携带了最关键的维度信息。比如“华东-1月-上海店.xlsx”,可以用str.extract把城市、月份、区域提出来,合成新列:

df["区域"] = df["来源文件"].str.extract(r"^(华东|华南|华北)") df["月份"] = df["来源文件"].str.extract(r"(\d+)月") df["城市"] = df["来源文件"].str.extract(r"-(\w+?)\.xls")

这里的正则按实际命名规则调整就行。为什么要做这一步?因为合并之后你很可能要按月份、区域、城市去看销售额,如果这些信息没从文件名里拆出来,你就只能重新回原文件去翻,那这个合并脚本的复用价值就少了一大半。

5.2 常用的分组统计和透视

处理大量Excel汇总时,最后产出往往不只是“一张大表”,而是一张能直接交给老板看的统计报表。Pandas的groupby和pivot_table正是干这个的:

# 按门店求和 store_sum = df.groupby("门店", as_index=False)["销售额"].sum() # 按月求销售额和订单量 month_sum = df.groupby("月份", as_index=False).agg( 销售额=("销售额", "sum"), 订单量=("单号", "count") ) # 区域×月份的透视表 pivot = pd.pivot_table( df, values="销售额", index="区域", columns="月份", aggfunc="sum", fill_value=0 )

这套操作的好处是,当你从几百个Excel里合并出了统一格式的数据之后,后续所有分析都可以基于同一个DataFrame快速变换,不再需要去原始文件里再来一次。

这里有个注意点:groupby默认会把分组列变成索引,如果后续要写入Excel并保持行列清晰,建议加as_index=False或者事后reset_index()。我见过太多人写完透视发现第一列变成了索引,然后到Excel里手工补一列“门店”,多此一举。

5.3 按维度拆成多个sheet或文件

与汇总相反,有时还需要把大表拆开。比如按门店拆成多个sheet,或者按月拆成多个文件。用ExcelWriter做这件事非常简单:

with pd.ExcelWriter("按门店拆分.xlsx") as writer: for store, group in df.groupby("门店"): # sheet名不能超过31字符且不能含特殊字符 sheet_name = str(store)[:31].replace("/", "-") group.to_excel(writer, sheet_name=sheet_name, index=False)

拆文件的写法也类似,把writer换成文件路径,在循环里写group.to_excel(f"拆分/{store}.xlsx", index=False)就行。

这个功能在业务端特别实用:总部汇总完数据,常常需要把各门店自己的明细单独发回去。一条groupby循环搞定,再也不用手工按门店筛选、复制、新建文件了。

5.4 输出格式不要过度美化

最后想提一句输出阶段的取舍。Pandas处理的是“数据”,不能指望它像真正的Excel报表那样做出漂亮的页眉、字体颜色、列宽、数据条。硬要用代码调样式也不是不行,但工作量成倍上涨、维护成本极高。

我的建议是:脚本只负责把数据合并准确、结构清晰、统计到位,最终的美化工作放到Excel里用模板功能或者人工微调。别在一个汇总脚本里试图解决所有问题,把“跑数据”和“做报表”这两件事分开,效率高得多。每次我想用openpyxl去给合并结果加边框和背景色的时候,最后都会发现这不值得。

6. 一些个人习惯和小技巧送给你

最后分享几个我自己跑多了这类任务之后沉淀下来的习惯,每一条都是实打实换来的。

第一,先处理单文件,再上循环。刚写好读取逻辑时,不要直接闭着眼跑整个文件夹,先挑一两个正常文件试一下,确认表头位置、列名、读取结果都符合预期,再放开循环全量跑。这一步能帮你省下大量的试错成本。

第二,记录失败的“刺头文件”。批量场景下总有几个文件读不了,千万别让脚本直接停掉,try/except包住整个处理流程,用列表记下出错的文件名和原因。脚本跑完直接就能拿到一份“失败清单”,对照清单去处理那几个特殊的文件,而不是对着黑压压的报错日志干瞪眼。

第三,保留一份原始文件的“档案”。合并输出之后,一定要有一列能追溯到数据来源的字段。别嫌麻烦,等到老板问你“这个月数怎么比上个月少了两万”的时候,你会感恩自己加了“来源文件”这一列。那句经典的“这个数据是谁给我的”在Excel合并场景里同样成立。有了来源列,按文件去拆分对比都是几行代码的事。

第四,脚本参数化。别写死文件路径和输出路径,把输入目录、输出文件、要处理的sheet名都抽成函数参数或者放在脚本最前面。下个月新一批报表,只需要把文件丢进同一个目录,改两行配置重跑一遍就行。把这个脚本保存好,它就是属于你的“Excel合并神器”。

这些技巧看起来都很小,但组合在一起,就是一套能让你从几百个Excel报表中解放出来的完整方案。Pandas能做的事远不止合并,可“大量Excel汇总”是最容易入门、也最容易看到实际收益的场景。按这套流程跑通一次之后,你大概率会像我一样,看到类似的重复劳动需求,第一反应不再是打开Excel,而是问一句:文件在哪?我写个脚本。

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

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

立即咨询