Python Excel 读取合并单元格真实值方法
用openpyxl或pandas读取带合并单元格的Excel时,只有左上角有值,其他位置都是None。本篇讲清怎么检测合并区域、把真实值填充到整行/整列,避免后续统计错。
场景痛点
领导丢过来一张表,表头是合并的"华东大区"横跨三列、每个部门合并了若干行。你用 pandas 一读,发现除了左上角那格有"华东大区",下面对应的几行全是 NaN。再做 groupby 统计,部门直接对不上号。问题不在数据丢了,而在于 Excel 合并单元格本来就只在左上角存值,其他格子是空的。读取时必须自己把值"铺开",后续统计才不会错。
用到的库
pip install openpyxl pandas
完整代码
# -*- coding: utf-8 -*-
"""
读取带合并单元格的 Excel,把真实值填充到每个被合并的格子上
"""
from openpyxl import load_workbook
from openpyxl.utils import range_boundaries
import pandas as pd
def make_demo(path: str):
"""先生成一张带合并单元格的示例表,方便后面演示读取"""
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
ws.title = "销售"
# 表头:A1 合并 A1:B1 写"大区"
ws["A1"] = "大区"
ws.merge_cells("A1:B1")
ws["C1"] = "销售额"
# 第 2~4 行:华东 合并 A2:A4
ws["A2"] = "华东"
ws.merge_cells("A2:A4")
ws.append([None, "产品A", 100])
ws.append([None, "产品B", 200])
ws.append([None, "产品C", 150])
# 第 5~6 行:华北 合并 A5:A6
ws["A5"] = "华北"
ws.merge_cells("A5:A6")
ws.append([None, "产品A", 120])
ws.append([None, "产品B", 180])
wb.save(path)
print("示例文件已生成:", path)
def read_with_openpyxl(path: str):
"""方案一:openpyxl 手动展开合并单元格"""
wb = load_workbook(path)
ws = wb["销售"]
print("\n===== 原始读取(不处理合并单元格)=====")
for row in ws.iter_rows(min_row=2, max_row=7, values_only=True):
print(row)
# 遍历所有合并区域,把左上角的值写回到区域内每个单元格
print("\n===== 展开后读取 =====")
for merged_range in list(ws.merged_cells.ranges):
min_col, min_row, max_col, max_row = range_boundaries(str(merged_range))
top_left_value = ws.cell(row=min_row, column=min_col).value
# 先取消合并,否则写不进去
ws.unmerge_cells(str(merged_range))
# 把值填到整个区域
for r in range(min_row, max_row + 1):
for c in range(min_col, max_col + 1):
ws.cell(row=r, column=c).value = top_left_value
for row in ws.iter_rows(min_row=2, max_row=7, values_only=True):
print(row)
def read_with_pandas(path: str):
"""方案二:pandas 读取,用 ffill 向下填充"""
df = pd.read_excel(path, sheet_name="销售", header=0)
print("\n===== pandas 原始读取 =====")
print(df)
# 合并单元格向下填充:把 NaN 用上面最近的非空值补上
df["大区"] = df["大区"].ffill()
print("\n===== pandas ffill 后 =====")
print(df)
# 此时可以正常分组统计
print("\n===== 按大区分组求和 =====")
print(df.groupby("大区")["销售额"].sum())
def main():
file_path = "merged_demo.xlsx"
make_demo(file_path)
read_with_openpyxl(file_path)
read_with_pandas(file_path)
if __name__ == "__main__":
main()
代码讲解
- Excel 合并单元格的存储规则:合并后,只有左上角那个格子保留原值,区域内其他格子在文件里就是空的。所以 pandas 读出来对应位置是 NaN,这不是 bug。
- openpyxl 方案:
ws.merged_cells.ranges是所有合并区域集合。对每个区域用range_boundaries解出(min_col, min_row, max_col, max_row),再拿左上角的值。 - 关键一步:必须先
ws.unmerge_cells(...)取消合并,否则直接往区域内其他格子写值会报错或被忽略。写完如果你要保留合并样式,可以重新merge_cells回去;本例只是为了读取,不合并也行。 - 双重循环
for r in range(...) for c in range(...)把值铺满整个矩形区域。 - pandas 方案更简单:
df["大区"].ffill()(forward fill)把上面最近的非空值向下填充,对"纵向合并"的场景一行搞定。前提是合并方向是纵向的(同一列跨行)。如果是横向合并(同一行跨列),得用ffill(axis=1)。 - 填完之后再做
groupby、sum就正常了,不会因为 NaN 把"华东"那一坨数据丢掉。
运行结果
生成 merged_demo.xlsx。控制台先打印原始读取:第二行开始"大区"列只有 A2 是"华东",A3/A4 是 None;展开后每行都带"华东"或"华北"。pandas 部分同样演示 NaN 被 ffill 补回,最后按大区分组求和:华东 450、华北 300。
注意事项
- 纵向合并用
ffill(),横向合并用ffill(axis=1),别搞反。 - 多层嵌套合并(合并里再合并)少见,真遇到要按"从大到小"的顺序处理,否则小区域会被大区域的填充覆盖。
- 用 openpyxl 取消合并并填充值后,如果再保存,原文件就被改成"无合并但每行都有值"了。想保留原表,请另存一个新文件处理,别覆盖原文件。
pd.read_excel默认会把第一行当表头;如果表头自己也是合并的,要传header=None然后自己命名列,否则列名会错位。- 大文件(几十万行)时,pandas ffill 比 openpyxl 双重循环快得多,优先用 pandas。
- 合并单元格在数据清洗里是"万恶之源",能劝业务方不合并就不合并;实在拿到了,按本篇方法处理即可。

更新时间:2026-09-15 09:03:42