200字
合并单元格Excel匹配工具
2026-02-11
2026-02-11

PixPin_2026-02-11_09-31-22.png

合并单元格匹配工具:从“合并单元格”到稳定的 Key→Value 映射

这篇文章记录一个在业务里非常常见、但在实现上很容易踩坑的需求:从带有合并单元格的 Excel 表中抽取 Key→Value 映射,再把匹配结果写回到另一个 Excel 表中。项目以 PyQt5 提供界面(exe 可打包发布),核心匹配逻辑基于 openpyxl 实现。

本文不包含任何本地路径或 IDE 跳转链接,方便直接复制到任意博客平台。仓库入口说明见 README.md,核心逻辑主要在 excel_merge_match.py

Github源码下载链接:合并单元格的Excel匹配


1. 业务痛点:合并单元格导致“读出来是空”

在 Excel 中,合并单元格的值只会存储在合并区域左上角“锚点”单元格里;区域内其他单元格的值通常是 None。这会带来一个直接问题:

  • 你看到表格视觉上某列每行都有值
  • 但程序逐行读取时,某些行的 key 列读取出来是空,于是匹配 key 断裂、映射丢失

因此,“正确处理合并单元格”是这个工具的核心能力之一。


2. 总体方案:两阶段处理

整个流程分成两步:

  1. 从“数据源 Excel”构建映射 mapping: Dict[str, Union[str, int, float]]
  2. 遍历“匹配源 Excel”,按 key 查表,把 value 写入指定列(或新增列)

对应实现主要是两个函数(都在 excel_merge_match.py):

  • build_source_mapping(src_cfg):从“数据源 Excel”构建 key→value 映射
  • apply_mapping_to_target(...):把映射写回到“匹配源 Excel”,并输出新文件

为了让逻辑清晰可测,配置以两个不可变 dataclass 表达:

  • SourceConfig:数据源配置(文件、sheet、key 列、value 列、是否累加)
  • TargetConfig:目标表配置(文件、sheet、key 列、写入列)

3. 表头与列选择:用“列名”而不是“列号”

在业务数据里,用户往往只知道“列名是什么”,而不是 A/B/C 或具体列号。这里采取的策略是:

  • 默认用第一行作为表头
  • 如果表头为空,则自动命名为 列{idx}(例如 列1、列2)
  • 以“列名→列号”字典做解析

相关实现函数(都在 excel_merge_match.py):

  • _header_to_col(ws):从第一行生成“列名→列号”的映射
  • _resolve_column(header_to_col, col_name) / _resolve_columns(...):按列名解析列号(找不到就抛错)

列枚举能力用于 UI 端的下拉框/多选框:

  • list_sheets(xlsx_path):列出所有 sheet
  • list_columns(xlsx_path, sheet_name):列出第一行的列名(空表头会补成 列{idx}

4. 合并单元格展开:构建“坐标→锚点”查找表

核心思路是先扫描工作表的所有合并区域,构建一个 lookup:

  • key:任意单元格坐标 (row, col)
  • value:该单元格所在合并区域的锚点坐标 (min_row, min_col)

实现函数:_build_merged_lookup(ws)

然后读取单元格值时遵循:

  1. 如果当前单元格本身有值,直接返回
  2. 如果当前单元格为空,且存在合并锚点,则回退到锚点读取
  3. 其他情况返回 None

实现函数:_get_cell_value(ws, merged_lookup, row, col)

这样就实现了“视觉上每行都有值”的效果:合并区域中的每行都能读到同样的 key。


5. Key 的构建:多列拼接 + 规范化

Key 支持多列组成,拼接规则是用 _ 连接:

key = "_".join([k1, k2, k3])

关键点有两个:

5.1 文本规范化

所有 key 部分会被转成字符串并 strip(),避免因为尾部空格导致匹配失败。实现函数:_normalize_text(v)

5.2 过滤“缺失 key”的行

任意一列 key 为空,就认为 key 不完整,跳过该行(避免生成脏 key)。这个规则在构建映射(build_source_mapping)与写回匹配(apply_mapping_to_target)两处都生效。


6. Value 的处理:支持“累加”模式

Value 的规范化规则是:

  • None 视为无效值,跳过
  • 数字(int/float)保留数值类型
  • bool 转成字符串(避免与数值逻辑混淆)
  • 其他类型按字符串 strip(),空串视为无效值

实现函数:_normalize_value(v)

6.1 非累加模式:后写覆盖

accumulate=False 时,同一 key 发生重复,后面的值会覆盖前面的值(见 build_source_mapping 的主循环)。

6.2 累加模式:数值求和 / 文本拼接去重

accumulate=True 时:

  • 如果 old/new 都是数值:求和(int+int 仍为 int,否则转 float)
  • 其他情况:用 ; 拼接,并按分号拆分后做去重

实现函数:_accumulate(old, new)

以及分号拆分器:_split_parts(s)

这与 README 的规则描述一致(数值求和 / 非数值用 ; 拼接并去重)。


7. 写回策略:写到已有列或新增列

写回时支持两种模式:

  1. 用户指定写入列名:写到该列(列必须存在,否则抛 KeyError)
  2. 用户不指定写入列:自动新增到最后一列,并写入表头

对应实现函数:apply_mapping_to_target(...)

表头命名规则:

  • 优先使用 output_header
  • 为空则使用 匹配_{Value列名}(Value 列名来自 src_cfg.value_column

另外,为了方便输出到任意目录,保存前会确保输出目录存在(逻辑同样位于 apply_mapping_to_target 内)。


8. 一个最小“脚本化”调用示例

GUI 通常会把用户选项转成配置对象,然后调用这两个函数。下面给出一个纯脚本方式的例子(仅用于理解调用形态):

from excel_merge_match import (
    SourceConfig,
    TargetConfig,
    apply_mapping_to_target,
    build_source_mapping,
)

src_cfg = SourceConfig(
    xlsx_path="source.xlsx",
    sheet_name="Sheet1",
    key_columns=("物料编码", "批号"),
    value_column="数量",
    accumulate=True,
)

mapping = build_source_mapping(src_cfg)

tgt_cfg = TargetConfig(
    xlsx_path="target.xlsx",
    sheet_name="Sheet1",
    key_columns=("物料编码", "批号"),
    write_to_column=None,
)

matched, total = apply_mapping_to_target(
    src_cfg=src_cfg,
    mapping=mapping,
    tgt_cfg=tgt_cfg,
    output_path="target_out.xlsx",
    output_header="匹配数量",
)

print(matched, total)

9. 边界情况与注意事项

  • 表头重复:当前策略是“同名列只取第一次出现的位置”(避免歧义),由 _header_to_col 保证
  • Key 拼接冲突:使用 _ 拼接时,如果 key 列本身包含 _,理论上可能造成不同组合得到同一字符串;如果在你的数据里会发生,建议在 UI 里提供“分隔符”可配置或使用不可见分隔符。
  • 大文件性能:_build_merged_lookup 会为每个合并区域内每个格子建立映射,合并区域特别大时可能占用较多内存;但这换来了读取阶段非常简单的 O(1) 回退查找。
  • openpyxl 的 data_only=True:意味着读取到的是“公式计算后的值”(如果文件中有缓存结果);如果源文件公式未计算过,可能会读到 None。这属于 openpyxl 的常见特性。

10. 依赖与打包简述

依赖见 requirements.txt

  • PyQt5:界面与交互
  • openpyxl:Excel 读写与合并单元格处理

Windows 打包产物与说明见 README 的“最小化打包(Windows)”章节。

合并单元格Excel匹配工具
作者
一晌小贪欢
发表于
2026-02-11
License
CC BY-NC-SA 4.0

评论