资讯动态

pandas批量合并Excel实战:字段映射、数据清洗与自动化

发布时间:2026/9/17 15:04:28 来源:尧图企业网站定制
简介面向需要处理datalog日志数据的工程师与C#开发者这份基于C#的Excel数据标记工具源码包旨在解决大规模数据中fail项难以快速识别与标记的问题。工具通过编程方式遍历单元格、判断条件并自动标红显著降低人工逐行核对成本适用于系统日志、工业检测等需要批量筛选异常数据的场景。包体共77个文件约11.11MB包含cs源码工程、sln解决方案、dll运行库、xml与json配置以及exe可执行文件等其中EPPlus等第三方库组件已一并附带便于打开工程后直接编译运行或按需修改。当前已有67人学习下载对于自动化办公与数据处理方向的学习者是一份可直接参考的完整示例。通过阅读工程中的FrmMarkData窗体与DataLogMark核心逻辑可掌握C#调用Excel操作库进行读取、判断、标记与保存的完整流程也可基于标记规则配置扩展自定义功能提升二次开发效率。1. 批量合并 Excel 文件的正确姿势从数据清洗到 Table 结构对齐日常工作中我们经常遇到需要把几十个结构相似但又不完全一致的 Excel 文件合并成一个总表的需求。可能是各分公司的销售报表也可能是不同月份的库存清单。手工复制粘贴不仅效率低下还容易在反复的打开、筛选、复制中出错。这个基于 Python 的 Excel 数据批量合并工具解决的正是这个痛点把散落在多个文件里的结构化数据按照统一的字段映射关系清洗、对齐、合并成一个干净的数据表。它的核心价值不在于“合并”这个动作本身而在于处理合并过程中那些让人头疼的“不一致”表头命名不同、列顺序不同、数据类型混乱、空值处理策略不同。适合数据处理岗位的同事、需要定期汇总报表的运营人员以及想要把重复性工作自动化的一线工程师。2. 合并之前先想清楚数据对齐的逻辑与选型2.1 为什么直接concat不行表头对齐的三个层次很多人第一反应是用 pandas 的pd.concat直接把多个 DataFrame 纵向拼接。这在所有文件表头完全一致时没问题但现实场景中你遇到的情况往往更复杂。需要把对齐问题拆成三个层次来考虑。第一个层次是列名不完全一致。比如一个文件里叫“客户名称”另一个文件里叫“客户名”还有一个叫“Company Name”。直接合并会产生三列不同的字段数据全乱了。这种问题需要建立一个字段映射关系表把不同叫法映射到统一的字段名上。第二个层次是列顺序不同。A 文件的第一列是日期B 文件的第一列是客户名虽然字段都有但直接 concat 会导致数据错位。必须在合并前统一列的顺序。第三个层次是数据类型不一致。同一个“金额”字段有的文件里是数字有的是文本有的甚至带上了货币符号。这些类型问题在合并后会导致计算错误。2.2 技术选型为什么用 pandas 而不是 openpyxl 或 xlrd处理 Excel 文件的 Python 库主要有三个xlrd、openpyxl 和 pandas。xlrd 对旧版.xls格式读取效率高但已经停止对.xlsx的支持而且它只负责读不负责写。openpyxl 能同时读写.xlsx精确控制单元格样式但它的 API 是单元格级别的做数据统计和清洗时你得自己写很多循环逻辑。pandas 的底层虽然也依赖 openpyxl 或 xlrd 做文件解析但它把 Excel 文件抽象成了 DataFrame——一个自带索引、数据类型推断和丰富清洗方法的数据结构。对于“批量合并”这个任务核心操作是“对齐”和“变换”DataFrame 的矢量运算能力可以省掉大量循环代码。如果你需要合并后保留原始文件的单元格格式、合并单元格、条件格式这些样式信息那 pandas 就无能为力了必须回到 openpyxl 逐行复制数据并设置样式。但多数数据合并场景只需要纯数据结果这种情况下 pandas 是最合适的选择。2.3 最小可用版本6 行代码先把流程跑通在写复杂逻辑之前先做一个最简版本验证合并链路是否通畅。代码如下import pandas as pd from pathlib import Path # 定义文件路径和合并结果路径 data_dir Path(./data/sales) files list(data_dir.glob(*.xlsx)) # 读取所有 Excel 文件并纵向合并 df_list [pd.read_excel(f) for f in files] merged pd.concat(df_list, ignore_indexTrue) # 输出合并结果 merged.to_excel(./output/merged_raw.xlsx, indexFalse)这段代码的思路是先用data_dir.glob(*.xlsx)获取目录下所有 Excel 文件路径然后用列表推导式逐个pd.read_excel读取最后用pd.concat纵向拼接。ignore_indexTrue的作用是重置索引否则合并后的 DataFrame 会保留每个原始文件的旧索引导致行号重复。to_excel里的indexFalse是防止把索引列也写进 Excel 文件。这个最简版本能跑通大部分“表头完全一致”的合并场景。如果你的文件里有一些隐藏 sheet 或者汇总小计行这个版本会把它们也读进来。后续章节我们会逐层增强这个基础流程。3. 解决表头不一致问题字段映射与列顺序统一3.1 建立外部映射表不要硬编码在代码里上一节的代码里没有处理列名不一致的问题。常见做法是建立一个独立的映射配置文件而不是把对应关系写在 Python 代码里。因为字段对应关系很可能会随着业务文件的变动而调整把映射放在代码里意味着修改一次就要改一次代码并重新部署放在外部文件里可以让不熟悉代码的同事直接编辑 Excel 或 JSON 维护映射关系。一个实用的方案是用 JSON 文件维护映射。每个 key 是标准化后的字段名value 是这个字段在不同来源文件里可能出现的所有别名列表。{ customer_name: [客户名称, 客户名, Company Name, customer], order_date: [订单日期, 下单时间, Date, order_date], amount: [金额, 销售额, Total, amount], sales_rep: [销售员, 业务员, Rep, sales_person] }读取这个映射文件并把它应用到 DataFrame 上核心逻辑是给每个文件做一次列名重命名。import json import pandas as pd def load_rename_mapping(json_path): 从 JSON 文件加载字段映射关系 with open(json_path, r, encodingutf-8) as f: mapping json.load(f) # 反转为 {别名: 标准名} 的字典便于直接用于 df.rename() rename_dict {} for standard_name, aliases in mapping.items(): for alias in aliases: rename_dict[alias] standard_name return rename_dict rename_dict load_rename_mapping(./config/field_mapping.json) def normalize_columns(df, rename_dict): 将 DataFrame 的列名标准化为统一字段名 # 只重命名在映射表中出现的列未匹配的列保留原名 df df.rename(columnsrename_dict) return df这里的逻辑是基于“每个别名唯一对应一个标准字段名”的假设。当有多个文件时每个文件读取后都先执行一次normalize_columns经过别名替换后各个文件就拥有了统一的列名集合。df.rename只会替换存在的列名如果某些列不在映射表中它会原样保留这样你还能发现哪些列没有被映射到。3.2 对齐列顺序先取列名交集再按标准顺序排列列名统一之后还有一个问题不同文件的列顺序可能不同。比如 A 文件的列顺序是“客户名称、订单日期、金额、销售员”B 文件是“订单日期、客户名称、销售员、金额”。纵向合并前如果不统一列顺序concat 会按第一个 DataFrame 的列顺序为准后续文件的数据就会错位填入错误的列。解决思路是先定义一个标准的列顺序列表然后对每个 DataFrame 执行reindex。standard_columns [customer_name, order_date, amount, sales_rep] def align_columns(df, standard_columns): 按标准列顺序重排列缺失列填充 None # reindex 会按标准列重新排列缺失的列默认填充 NaN return df.reindex(columnsstandard_columns) # 假设 df_normalized 已经过 rename 处理 df_aligned align_columns(df_normalized, standard_columns)reindex(columnsstandard_columns)的作用是如果 df 里有 standard_columns 中的列就按这个顺序排列如果 df 里缺少某个标准列就新建一列用 NaN 填充。这样所有文件合并前就有了完全相同的列结构concat 时数据就不会错位了。这里有一个值得注意的场景你的文件里可能有标准列之外的“多余列”这些列在某些文件里有、某些文件里没有。reindex会直接把多余的列丢弃因为reindex的结果只包含传入的列集合。如果你希望保留这些动态列就需要先对所有文件做并集再进行列对齐。3.3 一个参数解决“合并后列名重复”问题当多列映射到同一个标准字段名时pandas 会自动在列名后面加上后缀。例如两个文件里分别有“客户名称”和“Company Name”两列且这两个列名都映射到了customer_name如果只是各自 rename不会有什么问题。但如果某个文件里已经有一个customer_name列rename 之后又恰好映射出另一个customer_name就会导致重复列。这时合并前检查一下重复列是必要的。def check_duplicate_columns(df): 检查 DataFrame 是否存在重复列名 dup df.columns[df.columns.duplicated()].tolist() if dup: raise ValueError(f存在重复列名: {dup}) return df在每一轮 rename 之后执行检查用显式报错代替静默覆盖这个习惯能帮你在数据合并的早期发现问题。4. 处理数据类型不一致与空值策略4.1 金额字段清洗从“¥ 1,234.50”到 1234.50不同人维护的 Excel 文件里同一字段的数据格式千差万别。金额字段最常见的问题是有的单元格是数值 1234.5有的是带千分位的文本 “1,234.50”还有的带了货币符号 “¥1234.5”。如果直接用 pandas 合并文本列会被读成 object 类型后续做求和或均值时会报错或者得到错误结果。正则表达式配合 astype 可以统一处理这类问题。import re def clean_amount_series(series): 清洗金额字段统一转换为浮点数 def clean_value(v): if pd.isna(v): return None # 如果是数字类型直接返回 if isinstance(v, (int, float)): return float(v) # 如果是字符串去掉货币符号、千分位和空白 s str(v).strip() # 去掉人民币符号、美元符号和空格 s re.sub(r[¥$,\s], , s) # 处理括号负数格式 (1234) 表示 -1234 if s.startswith(() and s.endswith()): s - s[1:-1] try: return float(s) except ValueError: # 解析失败时返回 NaN便于后续发现问题 return float(nan) return series.apply(clean_value) # 用法示例 df[amount] clean_amount_series(df[amount])这个函数的逻辑分为几个情况NaN 值直接保留为空数值类型直接转 float字符串类型先去掉货币符号、千分位逗号和空白字符然后处理会计中常见的括号负数格式最后用 float 转换。转换失败的值返回 NaN在合并后你可以集中检查哪些数据是不规范的。4.2 日期字段的隐式陷阱Excel 序列号与字符串互换Excel 存储日期的方式有两种一种是真正的日期格式单元格pandas 读出来是datetime64类型另一种是文本格式的日期字符串比如 “2024/3/20” 或 “2024年3月20日”。还有一种最隐蔽的情况某些导出工具把日期写成了 Excel 序列号——整数部分是天数小数部分是时间pandas 读出来会是一串整数比如 45250 表示 2023 年 11 月 20 日。import pandas as pd def normalize_date_series(series): 将日期字段统一为 YYYY-MM-DD 字符串格式 def parse_date(v): if pd.isna(v): return None # 处理 Excel 序列号数字 1 代表 1899-12-31 if isinstance(v, (int, float)): if 20000 v 60000: # 合理日期范围大约 1954-2064 return pd.Timestamp(1899-12-30) pd.Timedelta(daysint(v)) else: return None # 超出合理范围交给后续处理 # 字符串类型尝试多种格式解析 try: dt pd.to_datetime(v, errorsraise) return dt.strftime(%Y-%m-%d) except (ValueError, TypeError): # 尝试中文格式 try: s str(v).replace(年, -).replace(月, -).replace(日, ) dt pd.to_datetime(s, errorsraise) return dt.strftime(%Y-%m-%d) except (ValueError, TypeError): return None return series.apply(parse_date) df[order_date] normalize_date_series(df[order_date])这里 Excel 序列号的基准日期用的是1899-12-30而不是1899-12-31原因是 Excel 存在一个历史遗留的闰年 bug它错误地认为 1900 年是闰年所以日期序列号从 1 开始对应 1900-01-01实际计算时偏移量要从 1899-12-30 开始算才能对齐。这个细节值得特别注意否则合并后的日期会整体偏移一天而且这种数据错误很难被察觉。4.3 空值处理策略合并时保留原始空值和填充空值空值策略取决于下游分析需求。如果只是合并后做透视表空值可以保留pandas 的计算函数会自动跳过 NA。如果合并结果要导入数据库或数据仓库通常需要显式填充空字符串或其他默认值。这里建议在合并之后做一个统一的空值清洗而不是在合并前对每个文件单独处理。原因有两个合并前处理可能因为单个文件的数据量不同导致填充策略不一致合并后统一处理可以让你看到完整的空值分布再决定策略。merged[sales_rep] merged[sales_rep].fillna(未分配) merged[amount] merged[amount].fillna(0)数值字段用 0 填充文本字段用“未分配”或空字符串填充这是最常用的组合。如果你的场景是统计分析把金额字段的缺失值填成 0 会拉低平均值这时可以考虑保留 NaN 并用skipna机制让计算自动跳过缺失值。在代码里加一个参数开关来控制这个行为是一种更灵活的做法。5. 封装成可复用的合并工具配置驱动与异常隔离5.1 配置文件驱动完整流程前面各节解决了列名、列顺序、数据类型三个核心对齐问题。现在把所有逻辑整合成一个可复用的函数。这个阶段的重点是让外部调用者不需要懂 pandas 也能完成合并操作同时保留足够的参数让高级用户可以干预细节。import json import pandas as pd from pathlib import Path def merge_excel_files(data_dir, output_path, config_path, sheet_name0): 按配置批量合并 Excel 文件 Args: data_dir: 存放源文件的目录 output_path: 合并结果输出路径 config_path: 字段映射配置文件路径 sheet_name: 读取哪个 sheet默认第一个 # 加载映射配置 with open(config_path, r, encodingutf-8) as f: mapping json.load(f) rename_dict {} for std, aliases in mapping.items(): for alias in aliases: rename_dict[alias] std standard_cols list(mapping.keys()) # 遍历文件 frames [] errors [] for file_path in Path(data_dir).glob(*.xlsx): try: df pd.read_excel(file_path, sheet_namesheet_name, dtype_backendnumpy_nullable) df df.rename(columnsrename_dict) df df.reindex(columnsstandard_cols) frames.append(df) print(f[OK] {file_path.name} — {len(df)} 行) except Exception as e: errors.append((file_path.name, str(e))) print(f[FAIL] {file_path.name} — {e}) # 合并并输出 if frames: result pd.concat(frames, ignore_indexTrue) result.to_excel(output_path, indexFalse) print(f合并完成: {len(result)} 行, 输出至 {output_path}) else: print(没有成功读取任何文件) # 报告失败的文件列表 if errors: print(f{len(errors)} 个文件处理失败:) for name, err in errors: print(f {name}: {err}) if __name__ __main__: merge_excel_files( data_dir./data/sales, output_path./output/merged_final.xlsx, config_path./config/field_mapping.json )这个函数的关键设计有两个。第一个是循环隔离每个文件单独用 try-except 包裹如果某个文件读取失败不会中断整个合并流程所以其他正常文件可以继续处理。失败的记录会收集到errors列表最后集中输出方便你一次看到所有有问题的文件。第二个是dtype_backendnumpy_nullable这个参数它让 pandas 在读取时保留原始的空值信息而不强制填充后续你再做空值策略时会更可控。5.2 性能优化文件多时从read_excel到read_excel(..., nrows0)当源文件数量较多且每个文件都有几十 MB 时合并过程的性能瓶颈通常不在concat而在read_excel。一个常见的优化方法是先只读取每个文件的表头行来获取列名跳过数据行减少不必要的 IO 和类型推断。# 快速获取表头不加载数据 header_df pd.read_excel(file_path, nrows0)nrows0告诉 pandas 只读取文件的前 0 行也就是只返回表头。这个技巧在你需要“扫描大量文件来做字段映射校验”时非常实用。但要注意read_excel的引擎仍然需要打开并解析整个文件的结构所以这种优化的收益有限。真正大幅减少合并时间的方式是先处理为 CSV 中间格式再在最后把 CSV 合并结果转回 Excel——CSV 的读取速度远快于 xlsx适合只有纯数据、无格式要求的内部流水线。另一个常见的坑是文件里包含图片或嵌入对象。openpyxl 在读取这些内容时会额外消耗内存并降低速度如果确认不需要图片内容可以在读取时捕获异常并跳过。合并工具本身不需要对图片做什么因为 pandas 不会把图片读进 DataFrame但引擎解析时仍会产生开销。5.3 与循环数据采集和 UI 卡顿问题的关系数据处理与界面解耦有些人在写桌面工具时会把合并逻辑直接放进 UI 的事件处理函数里。如果合并的数据量较大界面会卡住不动这就是典型的“数据处理和 UI 刷新挤在同一个线程”的问题。对于 C# 上位机或 WPF 程序来说这种体验尤其明显。Python 里同理。如果你的合并工具需要做成带界面的程序建议把合并逻辑放到单独的线程或进程中执行UI 线程只负责状态更新。可以使用concurrent.futures.ProcessPoolExecutor把合并任务提交给子进程返回值只传进度和最终结果。from concurrent.futures import ProcessPoolExecutor def run_merge_with_ui(data_dir, output_path, config_path): with ProcessPoolExecutor(max_workers1) as executor: future executor.submit(merge_excel_files, data_dir, output_path, config_path) # 这里可以轮询 future.done() 或使用回调来更新 UI 状态 future.result()这个写法把耗时的 Excel 合并任务放到了独立进程里UI 线程可以继续响应用户操作。如果你只需要在命令行里跑批处理这个设计就不是必须的但值得知道。6. 进阶用扫码枪或其他外设直接触发合并任务6.1 扫码枪当作“一键合并”触发器在仓库、物流和零售场景里扫码枪是常见的外设。大多数扫码枪模拟键盘输入扫描一个条码就相当于快速输入一串字符后按回车。利用这一特性可以让扫码枪的扫描动作直接触发 Excel 合并任务扫到一个特殊条码比如包含特定标识符的字符串就执行合并脚本扫到一个文件编号就自动选择特定的配置文件。实现思路很简单读取标准输入把扫码枪输入的字符串当作指令来解析再匹配预设的操作。import sys import threading import queue def listen_trigger_events(command_queue): 监听扫码枪输入识别触发指令 buffer while True: char sys.stdin.read(1) if char \n or char \r: command buffer.strip() if command: command_queue.put(command) buffer else: buffer char def check_trigger_and_run(command_queue, merge_function): 主循环收到扫码指令后执行合并任务 while True: try: cmd command_queue.get(timeout1) if cmd.startswith(MERGE_): config_name cmd.replace(MERGE_, ) config_path f./config/{config_name}.json merge_function( data_dirf./data/{config_name}, output_pathf./output/{config_name}_merged.xlsx, config_pathconfig_path ) except queue.Empty: continue # 使用示例 cmd_queue queue.Queue() t threading.Thread(targetlisten_trigger_events, args(cmd_queue,), daemonTrue) t.start() check_trigger_and_run(cmd_queue, merge_excel_files)扫码枪通过标准输入模拟键盘每次扫描后会发送回车键程序用这个回车符作为事件边界。用独立线程读标准输入主线程轮询指令队列识别到MERGE_前缀的指令就从config/目录下加载对应的配置并执行合并。这种方式的优点是不需要改任何硬件配置扫码枪插上 USB 接口就能用也不涉及驱动开发和权限问题。如果你的合并任务很多可以给每个任务分配一个编码比如扫码内容MERGE_SUMMER_SALES就合并夏季销售数据。6.2 数据合并后自动生成校验报告合并任务不应该以输出 Excel 文件为终点。推荐在合并工具里加一个可选的校验环节检查合并前后的总行数是否对得上检查必填字段是否有缺失检查金额字段的和是否在合理范围内。def generate_validation_report(merged_df, source_dir): 生成合并结果的校验报告 lines [] lines.append(f合并文件数: {len(list(Path(source_dir).glob(*.xlsx)))}) lines.append(f合并总行数: {len(merged_df)}) # 检查关键字段的空值数量 for col in [customer_name, amount]: if col in merged_df.columns: missing merged_df[col].isna().sum() lines.append(f字段 {col} 缺失 {missing} 条 ({missing / len(merged_df) * 100:.2f}%)) # 金额异常检查负数和超大量级 if amount in merged_df.columns: negative (merged_df[amount] 0).sum() lines.append(f金额为负数: {negative} 条) report_text \n.join(lines) with open(./output/validation_report.txt, w, encodingutf-8) as f: f.write(report_text) return report_text这个校验报告可以作为合并任务的附属产出。若发现缺失率异常或出现大量负数金额报告会提示你回到源文件中检查数据质量而不是让异常数据在合并后悄悄污染整体结果。6.3 最后一步把合并工具注册为 Windows 右键菜单命令对于需要经常合并文件的同事把工具做成 Windows 右键菜单项可以少点好几下。基本原理是修改注册表在.xlsx文件的右键菜单里加入一个“用此工具合并”的选项。由于每个目录下的 Excel 文件集合不同实现方式是对聚集在同一个文件夹内的多个 Excel 文件执行批量合并。要拿到右键点击的是哪个文件所在的目录需要把文件路径作为参数传给脚本。注册表的方式涉及一定的系统权限和兼容性你可以根据实际环境自行决定是否要这一步。更稳妥且亲民的方案是提供一个带图形界面的启动器——用 tkinter 或 pyqt 做目录选择和输出路径选择再把生成的批处理文件放到桌面。对于需要长期在固定工作流中使用的人来说节省的时间非常可观。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价