Python 玩转 Excel 案例【第 26 期】:取消合并 + 智能填充数据
📌 案例说明
原始数据表(测试数据.xlsx):
A B C D
┌─────────────────────────────────┐
1 │ 销售数据报表 │
├──────────┬───────┬──────┬───────┤
2 │ 部门 │ 产品 │ 销量 │ 金额 │
├──────────┼───────┼──────┼───────┤
3 │ │ 手机 │ 100 │ 50000 │
│ 销售一部 ├───────┼──────┼───────┤
4 │ │ 电脑 │ 50 │ 250000│
│ ├───────┼──────┼───────┤
5 │ │ 平板 │ 80 │ 120000│
├──────────┼───────┼──────┼───────┤
6 │ │ 手机 │ 120 │ 60000 │
│ 销售二部 ├───────┼──────┼───────┤
7 │ │ 耳机 │ 200 │ 40000 │
├──────────┼───────┼──────┼───────┤
8 │ │ 电脑 │ 30 │ 150000│
│ 销售三部 ├───────┼──────┼───────┤
9 │ │ 手表 │ 60 │ 180000│
└──────────┴───────┴──────┴───────┘
这是 Excel 数据处理中非常常见的场景:表格中存在大量合并单元格。
原始数据中:
- • 部门列
A3:A5、A6:A7、A8:A9 分别合并,只有每组第一个单元格有值
这种结构便于阅读,但对数据分析来说是“灾难”——pandas 读取后会出现大量空值,无法正常分组、统计。
本期案例跟大家一起学习用 Python 自动填充合并单元格,让数据变得规整,为后续分析扫清障碍。
核心操作:识别合并区域 + 取消合并 + 批量填充
- • 核心逻辑:遍历所有合并区域,获取左上角的值,取消合并后将值填充到区域内的所有单元格
📜 完整代码
"""
合并单元格填充 - 自动识别并填充 Excel 中的所有合并单元格
使用 openpyxl 实现“取消合并 + 值填充”的自动化处理
"""
from openpyxl import load_workbook
def fill_merged_cells(input_path, output_path, sheet_name=0):
"""
专门用于填充合并单元格并保存
参数:
input_path: 原始Excel文件路径
output_path: 输出文件路径
sheet_name: 工作表名称或索引(整数按索引获取,字符串按名称获取)
"""
# 1. 加载工作簿(data_only=True 读取值而非公式)
wb = load_workbook(input_path, data_only=True)
# 2. 获取工作表
if isinstance(sheet_name, int):
ws = wb.worksheets[sheet_name] # 按索引获取
else:
ws = wb[sheet_name] # 按名称获取
# 3. 处理每个合并区域
# 使用 list() 复制一份,因为边遍历边修改会报错
for merged_range in list(ws.merged_cells.ranges):
# 3.1 获取合并区域左上角的值
top_value = ws.cell(row=merged_range.min_row,
column=merged_range.min_col).value
# 3.2 取消合并
ws.unmerge_cells(str(merged_range))
# 3.3 将值填充到原区域的每一个单元格
for row in range(merged_range.min_row, merged_range.max_row + 1):
for col in range(merged_range.min_col, merged_range.max_col + 1):
ws.cell(row=row, column=col, value=top_value)
# 4. 保存处理后的文件
wb.save(output_path)
wb.close()
print(f"处理完成!已保存到: {output_path}")
# 使用
if __name__ == "__main__":
fill_merged_cells('测试数据.xlsx', '测试数据_处理后.xlsx')
运行结果(测试数据_处理后.xlsx):
💡 结果分析:所有合并单元格都被填充完整。原来只有左上角有值的 A1、A3、A6、A8,现在对应的 B1、C1、D1、A4、A5、A7、A9 也都有了相同的值。数据变得“规整”,可以正常进行 pandas 读取和后续分析了。
第一步:导入所需库
from openpyxl import load_workbook
代码解释:
- •
from openpyxl import load_workbook
导入 openpyxl 库中的 load_workbook 函数。openpyxl 是 Python 操作 Excel 文件的核心库,能够读取、修改、保存 .xlsx 格式的文件。
为什么选择 openpyxl 而不是 pandas?
对于本案例,我们需要“修改”Excel 文件本身(取消合并 + 填充值),因此 openpyxl 是最佳选择。
💡 小贴士:如果只需要读取数据而不修改原文件,可以先填充再读取,或者使用 pandas 的 fillna(method='ffill') 方法。
第二步:加载工作簿
wb = load_workbook(input_path, data_only=True)
代码解释:
- •
load_workbook(input_path)
加载指定路径的 Excel 文件,返回一个 Workbook 对象。 - •
data_only=True
这个参数非常关键!它决定了读取单元格时获取的是公式还是计算后的值。
data_only 参数说明:
- •
data_only=True:读取单元格的计算结果 - •
data_only=False:读取单元格的公式本身
对于合并单元格填充,我们需要的是值而不是公式,所以这里需要设置为 True。
💡 小贴士:如果 data_only=True 读到的是 None,说明公式没有被计算过。这时需要先用 Excel 打开文件并保存,或者使用 openpyxl 的 wb.calculate() 强制计算。
第三步:获取工作表
if isinstance(sheet_name, int):
ws = wb.worksheets[sheet_name] # 按索引获取
else:
ws = wb[sheet_name] # 按名称获取
代码解释:
- • 为什么需要这个判断?
因为用户可能用两种方式指定工作表: - • 传入数字
0 → 表示“第一个工作表”(索引从0开始) - • 传入字符串
'Sheet1' → 表示“名称为 Sheet1 的工作表”
- •
isinstance(sheet_name, int)
检查 sheet_name 是否是整数类型。 - •
wb.worksheets[sheet_name]
通过索引获取工作表。wb.worksheets 是所有工作表的列表,[0] 是第一个。 - •
wb[sheet_name]
通过名称获取工作表。
为什么不直接写 ws = wb.active?
| | | | |
|---|
wb.active | | | | |
wb.worksheets[索引] | | | | |
wb['名称'] | | | | |
| 组合判断 | | | | 高 |
wb.active 返回的是Excel打开时默认显示的那个工作表(通常是第一个,但不一定是),无法灵活选择。组合判断让函数更加通用。
第四步:遍历合并区域
for merged_range in list(ws.merged_cells.ranges):
代码解释:
- •
ws.merged_cells.ranges
获取工作表中所有合并区域的对象集合。例如本例中有4个合并区域:A1:D1、A3:A5、A6:A7、A8:A9。 - •
list(ws.merged_cells.ranges)
把合并区域集合转换成列表(复制一份),避免遍历时修改原集合导致报错。
为什么要把 ranges 转换成列表?
ws.merged_cells.ranges 返回的是一个集合对象。循环中执行 ws.unmerge_cells() 会修改这个集合(因为取消合并后,该区域就不在合并区域集合中了)。如果直接遍历原集合,边遍历边修改会报错:
# ❌ 错误写法
for merged_range in ws.merged_cells.ranges:
ws.unmerge_cells(str(merged_range)) # 运行时会报错!
list() 的作用是:把原集合复制一份到新列表。遍历的是这个新列表(副本),修改的是原集合。这样既安全又清晰。
- •
merged_range
每次循环拿到的合并区域对象,包含以下属性:
| | |
|---|
min_row | | |
max_row | | |
min_col | | |
max_col | | |
第五步:获取左上角的值
top_value = ws.cell(row=merged_range.min_row, column=merged_range.min_col).value
代码解释:
- •
merged_range.min_row、merged_range.min_col
合并区域左上角单元格的行号和列号。例如 A1:D1: - •
ws.cell(row=..., column=...).value
获取指定单元格的值。cell() 方法中 row 和 column 参数都是从1开始的(和 Excel 的行列编号一致)。
为什么是左上角?
在合并单元格中,只有左上角的单元格有实际值,其他单元格都是空的。填充的目的就是把左上角的值“复制”到整个区域。
我们的数据表中,第1次循环(标题行):
merged_range = A1:D1
merged_range.min_row = 1
merged_range.min_col = 1
top_value = ws.cell(row=1, column=1).value # 获取 A1 的值 = '销售数据报表'
第六步:取消合并
ws.unmerge_cells(str(merged_range))
代码解释:
- •
str(merged_range)
将合并区域对象转换成字符串格式,例如 A1:D1。 - •
ws.unmerge_cells('A1:D1')
取消 A1 到 D1 的合并状态。取消后,原来的合并区域变成了4个独立的单元格。
为什么必须先取消合并?
在 openpyxl 中,合并区域内的非左上角单元格是 MergedCell 类型,其 value 属性是只读的。如果直接赋值会报错:
AttributeError: 'MergedCell' object attribute 'value' is read-only
必须先取消合并,将这些单元格变回普通的 Cell 对象,才能写入值。
第七步:填充值到所有单元格
for row in range(merged_range.min_row, merged_range.max_row + 1):
for col in range(merged_range.min_col, merged_range.max_col + 1):
ws.cell(row=row, column=col, value=top_value)
代码解释:
这是双重循环,外层遍历行,内层遍历列,覆盖整个合并区域。
以 A3:A5 为例:
merged_range.min_row = 3
merged_range.max_row = 5
merged_range.min_col = 1
merged_range.max_col = 1
# 外层循环:range(3, 6) → [3, 4, 5]
for row in [3, 4, 5]:
# 内层循环:range(1, 2) → [1]
for col in [1]:
ws.cell(row=row, column=col, value='销售一部')
执行过程:
| | | | |
|---|
| | | ws.cell(3,1,'销售一部') | |
| | | ws.cell(4,1,'销售一部') | |
| | | ws.cell(5,1,'销售一部') | |
以 A1:D1 为例(多列情况):
merged_range.min_row = 1
merged_range.max_row = 1
merged_range.min_col = 1
merged_range.max_col = 4
# 外层循环:range(1, 2) → [1]
for row in [1]:
# 内层循环:range(1, 5) → [1, 2, 3, 4]
for col in [1, 2, 3, 4]:
ws.cell(row=1, column=col, value='销售数据报表')
执行过程:
| | | | |
|---|
| | | ws.cell(1,1,'销售数据报表') | |
| | | ws.cell(1,2,'销售数据报表') | |
| | | ws.cell(1,3,'销售数据报表') | |
| | | ws.cell(1,4,'销售数据报表') | |
第八步:保存并关闭
wb.save(output_path)
wb.close()
print(f"处理完成!已保存到: {output_path}")
代码解释:
- •
wb.save(output_path)
将修改后的工作簿保存到指定路径。如果文件已存在,会覆盖。 - •
wb.close()
关闭工作簿,释放资源。这是良好的编程习惯。 - •
print(...)
输出完成提示,让用户知道处理结果。
完整流程可视化
处理前(原始数据):
处理过程:
合并区域1: A1:D1 → 填充到 B1、C1、D1
合并区域2: A3:A5 → 填充到 A4、A5
合并区域3: A6:A7 → 填充到 A7
合并区域4: A8:A9 → 填充到 A9
处理后:
📚 本期核心知识点
📍 知识点 1:data_only=True 读取值而非公式
wb = load_workbook('文件.xlsx', data_only=True)
| | |
|---|
data_only=True | | |
data_only=False | | |
📍 知识点 2:MergedCell 只读属性
在 openpyxl 中,合并区域内的非左上角单元格是 MergedCell 类型,其 value 属性只能读不能写:
# ❌ 会报错
ws.cell(row=4, column=1).value = '销售一部' # 如果A4还在合并区域内
# ✅ 正确做法:先取消合并,再赋值
ws.unmerge_cells('A3:A5')
ws.cell(row=4, column=1).value = '销售一部'
📍 知识点 3:边遍历边修改的陷阱
# ❌ 错误写法
for merged_range in ws.merged_cells.ranges:
ws.unmerge_cells(str(merged_range)) # 修改了集合本身,遍历会出错
# ✅ 正确写法
for merged_range in list(ws.merged_cells.ranges):
ws.unmerge_cells(str(merged_range)) # 遍历的是副本,安全
📍 知识点 4:工作表获取的两种方式
# 按索引获取(整数)
if isinstance(sheet_name, int):
ws = wb.worksheets[sheet_name]
# 按名称获取(字符串)
else:
ws = wb[sheet_name]
📍 知识点 5:合并区域对象的常用属性
merged_range.min_row # 起始行号
merged_range.max_row # 结束行号
merged_range.min_col # 起始列号
merged_range.max_col # 结束列号
str(merged_range) # 转为Excel坐标字符串,如 'A1:D1'
🔄 本案例核心流程
① 加载工作簿 → ② 获取工作表 → ③ 遍历合并区域 → ④ 获取左上角值 → ⑤ 取消合并 → ⑥ 填充所有单元格 → ⑦ 保存文件
加载 Excel (load_workbook)
↓
获取工作表 (按索引或名称)
↓
遍历所有合并区域 (list(ws.merged_cells.ranges))
↓
┌─────────────────────────────────────────────┐
│ 对于每个合并区域: │
│ 1. 获取左上角单元格的值 │
│ 2. 取消合并 (unmerge_cells) │
│ 3. 双重循环覆盖行列 │
│ 4. 将值写入区域内每一个单元格 │
└─────────────────────────────────────────────┘
↓
保存到新文件 (wb.save)
↓
输出“处理完成”
🗳️ 点单时间到! 🗳️
下期写什么?
你来定,我来写。
评论区见!👇
📦 资源获取提示
关注「码农自习室」,后台回复关键词 PythonExcel案例,即可获取本文完整代码及配套练习数据集,一起动手掌握高效数据操作的核心技巧!
❤️ 支持我们
如果本文对你有帮助,欢迎点赞 + 关注 + 点亮小红心,你的支持就是我们持续创作的最大动力~