乐于分享
好东西不私藏

Python 玩转 Excel 案例【第 25 期】:批量合并多个Excel文件,自动添加来源标识

Python 玩转 Excel 案例【第 25 期】:批量合并多个Excel文件,自动添加来源标识

Python 玩转 Excel 案例【第 25 期】:批量合并多个Excel文件,自动添加来源标识

📌 案例说明

在实际工作中,我们经常需要将多个结构相同的Excel文件合并成一个总表,同时还要知道每条数据来自哪个文件。比如:

  • • 各部门提交的月度报表汇总
  • • 各分公司的销售数据合并
  • • 电子签章导出的多份文档详情列表

本案例演示如何批量读取文件夹下所有Excel文件,将数据纵向堆叠合并,并自动添加一列“表名”来标识数据来源。

文件结构

D:/合并表格/├── 项目A.xlsx├── 项目B.xlsx├── 项目C.xlsx└── 合并结果.xlsx  (程序生成)

每个文件的格式(文档详情列表 sheet):

A
B
C
D
(空行可能有多行)
文档名称
文档编号
签署时间
签署人
合同_001.pdf
CON-001
2024/1/1
张三
合同_002.pdf
CON-002
2024/1/2
李四

⚠️ 注意:文件的第一行可能是空行或标题,第二行才是真正的表头。

需求

  1. 1. 读取文件夹下所有 .xlsx 文件
  2. 2. 每个文件读取指定的 sheet(文档详情列表)
  3. 3. 跳过第一行,以第二行作为表头
  4. 4. 清理数据中的空格和空字符串
  5. 5. 删除全空的行
  6. 6. 添加“表名”列,记录数据来源的文件名
  7. 7. 将所有数据合并成一个总表

合并后的效果(合并结果.xlsx):

表名
文档名称
文档编号
签署时间
签署人
项目A
合同_001.pdf
CON-001
2024/1/1
张三
项目A
合同_002.pdf
CON-002
2024/1/2
李四
项目B
合同_003.pdf
CON-003
2024/1/3
王五
项目B
合同_004.pdf
CON-004
2024/1/4
赵六
项目C
...
...
...
...

核心操作

  1. 1. 用 Path.glob() 遍历文件夹下所有Excel文件
  2. 2. 用 skiprows 跳过非表头行
  3. 3. 用 applymap + strip() 清理字符串前后空格
  4. 4. 用 dropna(how='all') 删除全空行
  5. 5. 用 insert(0, ...) 在第一列添加来源标识
  6. 6. 用 pd.concat() 纵向合并所有数据

关键库pandaspathlib


📜 完整代码

"""批量合并Excel文件工具功能:将文件夹下所有Excel文件的指定sheet合并成一个总表适用:多部门/多项目/多批次数据的汇总场景"""import pandas as pdfrom pathlib import Path# ========== 配置区域 ==========folder_path = Path("D:/合并表格")sheet_name = "文档详情列表"# =============================all_dfs = []print(f"正在扫描文件夹: {folder_path}")print(f"目标Sheet: {sheet_name}")print("-" * 50)# 遍历文件夹下所有xlsx文件for file in Path(folder_path).glob("*.xlsx"):    # 跳过程序生成的合并结果文件(避免循环读取)    if file.name == "合并结果.xlsx":        continue    try:        # 1. 读取Excel:跳过第一行,以第二行作为表头        df = pd.read_excel(file, skiprows=1, sheet_name=sheet_name)        # 2. 清理字符串:去掉前后空格        df = df.applymap(lambda x: x.strip() if isinstance(x, str) else x)        # 3. 将空字符串替换为NaN        df = df.replace(r'^\s*$', pd.NA, regex=True)        # 4. 删除所有列都为空的行        df = df.dropna(how='all')        # 5. 添加来源列:在第一列插入文件名(不含扩展名)        df.insert(0, "表名", file.stem)        all_dfs.append(df)        print(f"✓ 已读取: {file.name} - {len(df)} 行数据")    except Exception as e:        print(f"✗ 读取失败: {file.name} - 错误: {e}")print("-" * 50)# 6. 合并所有数据if all_dfs:    merged_df = pd.concat(all_dfs, ignore_index=True)    print(f"合并完成!")    print(f"总行数: {len(merged_df)}")    print(f"总列数: {len(merged_df.columns)}")    print(f"\n表头列名:")    for i, col in enumerate(merged_df.columns, 1):        print(f"  {i}. {col}")    # 7. 保存合并结果    output_file = folder_path / "合并结果.xlsx"    merged_df.to_excel(output_file, index=False)    print(f"\n✓ 合并后的文件已保存到: {output_file}")    # 8. 数据预览    print(f"\n数据预览(前5行):")    print(merged_df.head())else:    print("没有成功读取任何文件,请检查:")    print("  1. 文件夹路径是否正确")    print("  2. 是否有xlsx文件")    print("  3. sheet名称是否拼写正确")

运行输出示例

正在扫描文件夹: D:/合并表格目标Sheet: 文档详情列表--------------------------------------------------✓ 已读取: 项目A.xlsx - 156 行数据✓ 已读取: 项目B.xlsx - 203 行数据✓ 已读取: 项目C.xlsx - 98 行数据--------------------------------------------------合并完成!总行数: 457总列数: 5✓ 合并后的文件已保存到: D:/合并表格/合并结果.xlsx数据预览(前5行):   表名        文档名称        文档编号      签署时间   签署人0  项目A  合同_001.pdf   CON-001  2024/1/1   张三1  项目A  合同_002.pdf   CON-002  2024/1/2   李四2  项目B  合同_003.pdf   CON-003  2024/1/3   王五3  项目B  合同_004.pdf   CON-004  2024/1/4   赵六4  项目C  合同_005.pdf   CON-005  2024/1/5  孙七

第一步:导入所需库

import pandas as pdfrom pathlib import Path

库说明

  • • pandas:读取Excel、处理数据、合并文件
  • • pathlib:路径操作,比 os.path 更简洁、更现代化

第二步:配置路径和参数

folder_path = Path("D:/合并表格")sheet_name = "文档详情列表"

代码解析

  • • Path("路径"):创建一个路径对象,支持链式操作
  • • sheet_name:要读取的工作表名称(所有文件必须有一致的sheet名)

Path对象的优势

# 传统写法(os.path)import osfiles = os.listdir("D:/folder")for f in files:    if f.endswith(".xlsx"):        full_path = os.path.join("D:/folder", f)# Path写法(更优雅)for file in Path("D:/folder").glob("*.xlsx"):    print(file.name)      # 文件名    print(file.stem)      # 文件名(不含扩展名)    print(file.parent)    # 父文件夹路径

第三步:遍历文件夹中的所有Excel文件

all_dfs = []for file in Path(folder_path).glob("*.xlsx"):    if file.name == "合并结果.xlsx":        continue

Path.glob("*.xlsx") 详解

  • • glob() 是路径匹配方法,支持通配符
  • • "*.xlsx" 表示匹配所有以 .xlsx 结尾的文件
  • • 返回一个生成器,逐个产生匹配的文件路径

为什么要跳过 合并结果.xlsx

如果不跳过,程序会把上一次合并生成的 合并结果.xlsx 也读进来,导致:

  1. 1. 把历史汇总结果再次合并进去 → 数据重复
  2. 2. 可能造成无限循环(新合并的文件又被读入)

最佳实践:给输出文件取一个固定的名字,并在遍历时排除它。


第四步:读取Excel文件

df = pd.read_excel(file, skiprows=1, sheet_name=sheet_name)

参数说明

参数
含义
本例的值
file
文件路径
Path对象
skiprows=1
跳过前1行
跳过第1行(标题/空行),从第2行开始读
sheet_name
工作表名称
"文档详情列表"

为什么要 skiprows=1

原文件的格式:

第1行:可能是标题或空行(不需要)第2行:真正的表头(文档名称、文档编号、签署时间、签署人)第3行起:数据

如果不跳过第1行,表头就会错位:

# 错误示例:没有skiprowsdf.columns = ["乱七八糟的内容", ...]  # 第1行成了表头# 正确示例:skiprows=1df.columns = ["文档名称", "文档编号", "签署时间", "签署人"]  # 第2行成了表头

第五步:清理数据中的空格

df = df.applymap(lambda x: x.strip() if isinstance(x, str) else x)

applymap() 是什么?

  • • DataFrame 的一个方法,作用是对每一个单元格执行同一个函数
  • • 它会遍历整个表格(所有行、所有列),把每个单元格的值传给函数,然后用函数的返回值替换原值
  • • 相当于写了两层循环(遍历行 + 遍历列),但用一行代码搞定
lambda x: x.strip() if isinstance(x, str) else x

这是一个匿名函数,x 代表传入的单元格值。执行逻辑分两步判断:

  • • 先判断类型isinstance(x, str) 检查 x 是不是字符串
  • • 如果是字符串:执行 x.strip(),去掉字符串开头和结尾的空白字符(空格、制表符、换行符等),中间的空格保留
  • • 如果不是字符串(数字、日期、布尔值、None 等):直接返回 x 本身,不做任何处理

为什么要判断类型?

如果对数字调用 strip() 会直接报错。比如数字 100 没有 .strip() 方法,不判断类型的话程序会崩溃。

举例说明

原始 DataFrame:

姓名
工号
金额
" 张三 "
"001"
5000
"李四  "
"002"
6000
" 王五"
"  003  "
7000

经过 applymap 处理后:

姓名
工号
金额
"张三"
"001"
5000
"李四"
"002"
6000
"王五"
"003"
7000

注意金额列是数字,没有被 strip 影响。

一个容易被忽略的细节

strip() 只能去掉字符串前后的空格,单元格中间的空格会保留。比如 "张   三" 处理后还是 "张   三",中间多个空格不会被删掉。如果业务上需要统一中间空格,那是另一个话题,要用 replace 或正则处理。


第六步:将空字符串替换为NaN

df = df.replace(r'^\s*$', pd.NA, regex=True)

replace() 参数说明

参数
含义
r'^\s*$'
正则表达式:匹配全是空格的字符串
pd.NA
替换为目标值(pandas的空值标记)
regex=True
启用正则表达式匹配

正则表达式 r'^\s*$' 详解

符号
含义
^
字符串开头
\s*
0个或多个空白字符(空格、制表符等)
$
字符串结尾
r''
原始字符串,避免转义问题

匹配示例

  • • ""(空字符串) → 匹配 ✅
  • • "   "(多个空格) → 匹配 ✅
  • • "abc" → 不匹配 ❌

为什么要替换空字符串?

经过 applymap 的 strip() 处理后,原来只有空格的单元格变成了空字符串 ""。但 "" 不是 pandas 的空值标记,dropna 删不掉它。

举个例子

原始值
strip()后
replace()后
最终结果
"   "
 (三个空格)
""pd.NA
被 dropna 删除
""
 (空字符串)
""pd.NA
被 dropna 删除
NoneNonepd.NA
被 dropna 删除
" 张三 ""张三""张三"
保留
123
 (数字)
123123
保留
# 如果不做 replace,dropna 无效df = df.applymap(lambda x: x.strip() if isinstance(x, str) else x)df.dropna(how='all')  # 删不掉空字符串行!# 加上 replace 才能生效df = df.replace(r'^\s*$', pd.NA, regex=True)df.dropna(how='all')  # 生效了

一句话strip() 把空格变 "",但 dropna 只认 pd.NA,所以需要 replace 转一下。


第七步:删除全空的行

df = df.dropna(how='all')

dropna(how='all') 详解

参数
含义
how='all'
只有当一行所有列都是NaN时,才删除
how='any'
只要有一列是NaN,就删除(默认)

示例

# 原始数据   文档名称  文档编号  签署时间  签署人0  合同1    CON1   1/1   张三1  合同2    NaN   1/2   李四    # 部分空,保留2   NaN    NaN   NaN   NaN      # 全空,删除3   NaN    NaN   空字符串 NaN    # 空字符串已被转NaN,也全空,删除# dropna(how='all') 之后   文档名称  文档编号  签署时间  签署人0  合同1    CON1   1/1   张三1  合同2    NaN   1/2   李四

注意:经过上一步的空字符串替换后,原本的 "" 已经变成了 NaN,所以能被正确识别并删除。


第八步:添加来源标识列

df.insert(0, "表名", file.stem)

insert() 参数说明

参数
含义
0
插入位置(0表示第一列)
"表名"
新列的列名
file.stem
列的值(每个单元格都是这个文件名)

file.stem 的作用

# 假设 file = Path("D:/folder/项目A.xlsx")file.name   # "项目A.xlsx"  (含扩展名)file.stem   # "项目A"       (不含扩展名)

最终效果

每一行数据前面都会多一列"表名",标识这条数据来自哪个文件。

  表名    文档名称    文档编号    ...0 项目A   合同1.pdf  CON001   ...1 项目A   合同2.pdf  CON002   ...2 项目B   合同3.pdf  CON003   ...

实用价值:当合并后想追溯某条数据的来源时,这一列就是“身份证”。


第九步:收集所有数据

all_dfs.append(df)print(f"✓ 已读取: {file.name} - {len(df)} 行数据")

all_dfs 列表的结构

all_dfs = [    df_项目A,   # 156行 × 5列    df_项目B,   # 203行 × 5列    df_项目C    # 98行 × 5列]

第十步:纵向合并所有数据

merged_df = pd.concat(all_dfs, ignore_index=True)

pd.concat() 参数说明

参数
含义
all_dfs
要合并的DataFrame列表
ignore_index=True
重置行索引(0,1,2...),而不是保留原来的索引

ignore_index 的效果对比

# ignore_index=False(默认)df1索引: 0,1,2df2索引: 0,1,2合并后: 0,1,2,0,1,2  # 索引重复!# ignore_index=Truedf1索引: 0,1,2df2索引: 0,1,2合并后: 0,1,2,3,4,5  # 索引连续且唯一

推荐:合并时始终设置 ignore_index=True,避免索引混乱。


第十一步:保存合并结果

output_file = folder_path / "合并结果.xlsx"merged_df.to_excel(output_file, index=False)

/ 操作符的作用

pathlib 支持用 / 拼接路径,非常直观:

# 传统写法output = os.path.join(folder_path, "合并结果.xlsx")# Path写法output = folder_path / "合并结果.xlsx"

to_excel(index=False) 的作用

  • • index=False:不把行索引写入Excel
  • • 如果写成 index=True(默认),会在Excel最左边多出一列无名序号

第十二步:数据预览

print(merged_df.head())

head() 方法:默认显示前5行数据,快速检查合并结果是否正确。


📚 核心知识点总结

1. 遍历文件夹

for file in Path(folder).glob("*.xlsx"):    print(file.stem)   # 文件名(无扩展名)
  • • glob() 支持通配符匹配
  • • file.stem 获取不含扩展名的文件名

2. 跳过指定行读取

df = pd.read_excel(file, skiprows=1, sheet_name="Sheet1")
  • • skiprows=1 跳过第1行,以第2行作表头
  • • 适合文件前几行是标题/空行的情况

3. 字符串清理组合拳

df = df.applymap(lambda x: x.strip() if isinstance(x, str) else x)df = df.replace(r'^\s*$', pd.NA, regex=True)df = df.dropna(how='all')
  • • applymap + strip():去掉字符串前后空格
  • • replace + 正则:空字符串 → NaN
  • • dropna(how='all'):删除全空行

4. 添加来源列

df.insert(0, "来源", file.stem)
  • • insert(0, ...):在第一列插入
  • • 便于追溯每条数据的来源文件

5. 纵向合并

result = pd.concat(all_dfs, ignore_index=True)
  • • ignore_index=True:重置索引,避免重复

🔄 核心流程图

开始  ↓遍历文件夹中所有.xlsx文件  ↓跳过"合并结果.xlsx"  ↓读取Excel(skiprows=1跳过第1行)  ↓清理字符串空格(applymap + strip)  ↓空字符串 → NaN(replace + 正则)  ↓删除全空行(dropna how=all)  ↓添加"表名"列(insert)  ↓存入all_dfs列表  ↓是否还有文件? ──是──→ 继续遍历  ↓ 否pd.concat纵向合并  ↓保存为"合并结果.xlsx"  ↓打印预览信息  ↓结束

🛠️ 常见问题与解决方案

Q1:报错 No sheet named '文档详情列表'

A:检查每个Excel文件是否都有这个sheet,名称是否完全一致(注意空格和大小写)。

Q2:表头列数不一致导致合并出错

A:pd.concat() 会自动对齐列名,列数不同也能合并,缺失的列会填充NaN。

Q3:合并后总行数比预期多

A:检查是否没有跳过 合并结果.xlsx,导致把之前合并的文件又读了一遍。

Q4:空行没有被删除

A:确保先执行了 replace(r'^\s*$', pd.NA, regex=True),因为 dropna 只删除NaN,不删除空字符串。


🗳️ 点单时间到! 🗳️

下期写什么?你来定,我来写。


📦 资源获取提示

关注「码农自习室」,一起动手掌握高效数据操作的核心技巧!


🛠️ 更多工具

点击下方图片,进入小程序,随时查阅往期所有案例代码。

❤️ 支持我们

如果本文对你有帮助,欢迎点赞 + 关注 + 点亮小红心,你的支持就是我们持续创作的最大动力~