我的知识记录

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)
  • 填完之后再做 groupbysum 就正常了,不会因为 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。
  • 合并单元格在数据清洗里是"万恶之源",能劝业务方不合并就不合并;实在拿到了,按本篇方法处理即可。

Python Excel 读取合并单元格真实值方法

标签:

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

上一篇:Python Excel删除重复保留最新一条

下一篇:Python Excel设置打印区域页眉页脚:打印前一键排版