ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python Excel表格拼接合并实战:pandas多表合并与自动化处理指南

Python Excel表格拼接合并实战:pandas多表合并与自动化处理指南 1. 为什么“拼接合并”是Excel自动化里绕不开的一关日常处理表格的人迟早会撞上这个场景手头有十二个月的销售明细每个月一个文件或者一个项目拆成了十几个部门分表格式一模一样只是数据不同。这时候要把它们合成一张总表手动复制粘贴的后果就是——眼睛看花、行数对不上、漏掉某个文件还浑然不知。我见过太多人在这步上耗掉整个下午最后还因为粘贴时错位一行导致后面所有汇总全盘皆错。用Python对Excel表格进行拼接合并解决的就是这类“多表合一”的重复劳动。它的核心价值不在于代码有多复杂而在于把一件机械、易错、量大的事情变成一段跑一次就永远可靠的脚本。你只需要把规则想清楚剩下的交给代码几十个文件也就是几秒钟的事。这篇文章适合两类人一类是刚学完pandas基础、能读写单个Excel但还没系统处理过多文件的人另一类是已经在用Python做报表但每次合并都靠临时拼凑代码、没有形成稳定套路的人。我会从方案选型讲到具体实现把纵向堆叠、横向拼接、多Sheet合并、带格式的合并这几条路线都拆开讲透并且把我在实际项目里踩过的坑一并交代清楚。看完你至少能拿到一套可以直接改改就用的合并模板而不是每次重新造轮子。需要先明确一个概念“拼接”和“合并”在Excel语境里其实是两件事。拼接通常指把结构相同的表上下摞起来行数增加、列数不变英文里叫concat合并则可能涉及按某个共同列把两张表左右接起来类似SQL里的join。很多人一开始分不清导致用错方法。这篇主要聚焦前者——也就是最常被叫做“表格拼接合并”的纵向堆叠同时也会把横向拼接和按Sheet合并讲清楚因为实际工作里它们经常混在一起出现。2. 方案选型pandas、openpyxl还是直接上VBA动手之前先想清楚用什么工具这一步选错后面全是返工。2.1 三种主流路线的对比处理Excel多表合并常见的有三条路pandas、openpyxl、以及Excel自带的VBA。我把它们的适用场景整理成一张表你对号入座。方案优势短板适合场景pandas语法简洁、批量处理快、天然支持多表堆叠默认不保留原格式、样式会丢数据量大、只关心数据本身openpyxl能读写单元格、保留部分格式、可操作Sheet逐格操作慢、代码啰嗦需要保留格式、表头有合并单元格VBA在Excel内运行、无需环境跨文件处理弱、维护困难单文件内多Sheet、轻量需求我的建议很直接只要你的目标是“把数据合到一起做分析”无脑选pandas。它的concat就是为这件事设计的一行代码顶别人几十行。只有当表格里有大量合并单元格、颜色标记、公式需要原样保留时才考虑openpyxl。VBA我基本不推荐用于跨文件合并维护成本太高换个人接手就看不懂了。2.2 为什么pandas是首选pandas处理Excel合并的底层逻辑很清晰先把每个文件读成一个DataFrame可以理解成内存里的一张虚拟表再用concat把这些虚拟表按行或按列拼起来最后写回一个Excel。整个过程不碰Excel界面纯数据操作速度快且稳定。它最大的好处是规则统一。手动合并时你可能会因为某个文件多了一列、少了一行而手忙脚乱pandas在拼接时会自动对齐列名列名对不上的地方填NaN你一眼就能看出哪个文件有问题。这种“把异常暴露出来”的特性比手动粘贴时悄悄错位要安全得多。2.3 环境准备与依赖安装动手前把环境弄干净。我习惯用虚拟环境避免不同项目的库版本打架。python -m venv excel_env # Windows excel_env\Scripts\activate # macOS / Linux source excel_env/bin/activate pip install pandas openpyxl这里有个细节读xlsx用openpyxl读xls用xlrd。现在新版本pandas对xls的支持需要额外装xlrd而且xlrd新版本已经不再支持xlsx了。如果你手上还有老的xls文件记得补一句pip install xlrd1.2.0版本别装错否则会报错。写文件统一用openpyxl就够了。提示不要在同一环境里混装太多版本的pandas升级前先确认现有脚本能不能跑。我就吃过一次亏升级pandas后append方法被废弃一堆老脚本集体报错。3. 纵向拼接把多个结构相同的表摞成一张这是最核心、最高频的场景先把这块吃透后面都是它的变体。3.1 单文件夹批量读取的基本套路假设所有待合并的Excel都放在同一个文件夹里结构完全一致。标准做法分三步遍历文件夹拿到所有文件路径、逐个读成DataFrame、用concat堆叠。import pandas as pd import os from pathlib import Path folder Path(./sales_data) all_files list(folder.glob(*.xlsx)) frames [] for f in all_files: df pd.read_excel(f, engineopenpyxl) df[来源文件] f.name # 关键标记数据来源 frames.append(df) result pd.concat(frames, ignore_indexTrue) result.to_excel(合并结果.xlsx, indexFalse)这段代码里有两个地方值得展开说。第一是df[来源文件] f.name强烈建议加上这一列。合并之后如果发现某行数据有问题你能立刻定位是哪个文件来的。我做过一个项目合并了四十多个分表后来发现总数对不上全靠这列来源标记才快速揪出是一个文件被重复读取了。第二是ignore_indexTrue它让合并后的行号重新从0开始排否则每个文件的索引会原样带过来出现一堆重复的0、1、2看着就乱。3.2 表头不一致时的对齐处理现实里很少有那么理想的情况经常是A文件表头叫“销售额”B文件叫“销售金额”C文件多了个“备注”列。pandas的concat默认按列名对齐对不上的填NaN。这其实是好事但你需要主动检查。result pd.concat(frames, ignore_indexTrue, sortFalse) print(result.isnull().sum()) # 看每列有多少空值如果某列空值特别多八成是列名不统一导致的。这时候有两个选择要么在读取后统一重命名列要么在合并前做一次列名映射。rename_map {销售金额: 销售额, 金额: 销售额} for df in frames: df.rename(columnsrename_map, inplaceTrue)注意列名映射表要提前人工核对一遍别指望代码自动猜。我见过有人用模糊匹配自动改列名结果把“销售额”和“销售税额”匹配到一起数据全错。3.3 只读取需要的列和跳过表头行有些分表前面几行是标题、说明、制表人真正的表头在第3行甚至第5行。这时候read_excel的skiprows和header参数就派上用场了。df pd.read_excel(f, skiprows2, usecolsA:F, engineopenpyxl)skiprows2表示跳过前两行usecolsA:F表示只读A到F列。这两个参数配合使用能应付绝大多数“表头不在第一行”的情况。如果每个文件的表头行数还不一样那就得先读一遍探测或者干脆要求数据源统一格式——从源头规范永远比事后补救省事。3.4 处理超多文件时的内存与性能文件一多比如几百个全读进内存再合并可能会卡。这时候有两个优化方向。一是只读需要的列减少内存占用二是分批合并避免一次性堆太多DataFrame。result pd.DataFrame() for f in all_files: df pd.read_excel(f, usecols[日期, 产品, 销售额], engineopenpyxl) result pd.concat([result, df], ignore_indexTrue)不过要提醒一句在循环里反复concat其实效率不高因为每次都会创建新对象。文件数量在几十个以内用列表收集再一次性concat是最快的上百个以上可以考虑先合并成几个中间文件再二次合并。实测下来一百个每个几千行的文件列表收集法也就几秒钟完全够用。4. 横向拼接与按Sheet合并另外两种常见形态纵向堆叠解决了大部分问题但实际工作里还有两种变体经常出现单独拎出来讲。4.1 按共同列横向拼接merge有时候两张表不是上下摞而是左右接。比如一张表是产品编号和名称另一张是产品编号和库存要通过编号把库存接上去。这就是merge对应SQL的join。df1 pd.read_excel(产品信息.xlsx) df2 pd.read_excel(库存.xlsx) result pd.merge(df1, df2, on产品编号, howleft)how参数决定保留哪些行left保留左表全部right保留右表全部inner只保留两边都有的outer全保留。选哪个取决于业务逻辑。做报表时我一般用left以主表为准右表没有的填NaN这样不会莫名其妙多出或丢掉行。提示merge之前务必确认连接列没有重复值否则会产生笛卡尔积行数爆炸。我踩过一次左表编号唯一右表同一个编号出现两次结果合并后行数翻倍排查了半天才发现是右表有重复录入。4.2 一个文件内多个Sheet的合并还有一种情况是数据都在一个Excel里但分散在多个Sheet比如一月、二月、三月各一个Sheet。这时候用pd.read_excel的sheet_nameNone一次性读全部。sheets pd.read_excel(季度数据.xlsx, sheet_nameNone, engineopenpyxl) frames [] for name, df in sheets.items(): df[月份] name frames.append(df) result pd.concat(frames, ignore_indexTrue)sheet_nameNone返回的是一个字典键是Sheet名值是DataFrame。遍历这个字典就能把每个Sheet都拿到顺便把Sheet名作为一列“月份”加进去一举两得。这个方法比逐个指定Sheet名灵活得多尤其适合Sheet数量不固定的场景。4.3 跨文件跨Sheet的混合合并最复杂的情况是多个文件每个文件里又有多个Sheet全都要合到一起。这时候把前面两招组合起来就行。all_frames [] for f in all_files: sheets pd.read_excel(f, sheet_nameNone, engineopenpyxl) for name, df in sheets.items(): df[来源文件] f.name df[来源Sheet] name all_frames.append(df) result pd.concat(all_frames, ignore_indexTrue)加上“来源文件”和“来源Sheet”两列合并后任何一行数据都能追溯到它的出处。这套组合拳我在处理集团各分公司上报数据时用过几十个文件、每个文件好几个Sheet跑下来一气呵成比人工整理快了不止一个量级。5. 实操全流程从一堆乱表到一张干净总表前面讲的是分块知识这一节把它们串成一条完整的流水线。我以一个典型的“多个月份销售分表合并”为例把每一步的操作意图和参数选择都交代清楚。5.1 第一步摸清数据源的真实结构别急着写代码先花五分钟把文件夹里的文件看一遍。重点确认三件事文件格式是否统一都是xlsx还是混了xls、表头在第几行、列名是否一致。我习惯先写个探测脚本把每个文件的列名打印出来对比。for f in all_files: df pd.read_excel(f, nrows0, engineopenpyxl) print(f.name, list(df.columns))nrows0表示只读表头不读数据速度极快。把所有文件的列名列出来一对比哪些不一致一目了然。这一步看似多余实则能省掉后面大量调试时间。先探测再合并是专业和业余的分水岭。5.2 第二步统一列名与数据类型探测完发现列名有出入就在读取后统一重命名。同时要注意数据类型尤其是日期列和数字列。Excel里的日期读进来可能是字符串也可能是datetime不统一的话合并后会出问题。frames [] for f in all_files: df pd.read_excel(f, engineopenpyxl) df.rename(columnsrename_map, inplaceTrue) df[日期] pd.to_datetime(df[日期], errorscoerce) df[销售额] pd.to_numeric(df[销售额], errorscoerce) df[来源文件] f.name frames.append(df)errorscoerce的作用是遇到无法转换的值就置为NaN而不是直接报错中断。这样即使某个文件里有个别脏数据整个流程也能跑完最后通过检查NaN来定位问题。这比中途崩溃要友好得多。5.3 第三步合并并做完整性校验合并本身一行代码但合并后的校验才是保证质量的关键。result pd.concat(frames, ignore_indexTrue) print(总行数, len(result)) print(各来源文件行数) print(result[来源文件].value_counts()) print(空值统计) print(result.isnull().sum())value_counts()能看出每个文件贡献了多少行如果某个文件行数是0说明它没被正确读取如果某个文件行数异常多可能是重复读取了。空值统计则能发现列对齐问题。这几行检查代码花不了几秒钟却能挡住绝大多数低级错误。5.4 第四步去重、排序与写出合并后经常会有重复行尤其是多个文件有重叠数据时。去重和排序是收尾的标准动作。result.drop_duplicates(inplaceTrue) result.sort_values(by[日期, 产品], inplaceTrue) result.to_excel(合并总表.xlsx, indexFalse, engineopenpyxl)drop_duplicates默认比较所有列如果只想按某几列去重可以传subset参数。排序让最终的表更易读。写出时indexFalse避免多出一列无意义的行号。到这一步一张干净的总表就出来了。5.5 关键参数速查表把这一节涉及的核心参数整理成表方便你写代码时对照。参数作用常用取值sheet_name指定读取的SheetNone读全部、名称或索引skiprows跳过开头若干行整数usecols只读指定列A:F或列名列表ignore_index合并时重置索引Truesort按列名对齐排序False避免警告howmerge的保留策略left/right/inner/outererrors类型转换失败处理coerce6. 常见问题与排查技巧实录代码跑不通是常态关键是要有一套排查思路。下面这些是我这些年实际遇到并解决过的问题整理成速查表遇到报错先来这里找。6.1 报错与异常速查现象可能原因解决方向FileNotFoundError路径含中文或空格、glob没匹配到用Path对象、打印文件列表确认读出来全是NaN表头行判断错误调整header或skiprows列名带Unnamed表头有空单元格读取后重命名或删列日期变成数字Excel日期序列号未转换pd.to_datetime处理合并后行数翻倍merge连接列有重复先去重再merge写入报PermissionError目标文件正被打开关闭Excel再写6.2 那些文档里不会写的坑第一个坑是隐藏Sheet。有些Excel里有隐藏的工作表sheet_nameNone默认也会把它们读进来导致多出一批莫名其妙的数据。如果确认不需要读取后按Sheet名过滤掉。第二个坑是公式单元格。pandas读Excel默认读的是公式的计算结果但如果文件从没在Excel里打开过、公式没被计算读出来可能是None或0。这种情况要么先用Excel打开保存一次要么改用能计算公式的库处理。第三个坑是大文件的内存。一个几十万行的xlsx读进来可能占几百兆内存多个文件叠加容易爆。这时候可以考虑先转成csv再处理或者用chunksize分块读取。不过对于常规的几千到几万行完全不用担心。提示写出的文件如果还要给别人看建议在to_excel之后再用openpyxl调一下列宽否则默认列宽很窄数字显示成一片井号观感很差。6.3 让脚本更稳的几个习惯我现在的合并脚本都会加三个习惯动作。一是先备份原始文件合并只读不写原文件输出到新目录万一出错原始数据还在。二是打印进度文件多的时候每处理一个打印一次卡住了能知道卡在哪。三是异常捕获单个文件读取失败不要让整个脚本崩掉记录下文件名跳过最后统一报告。failed [] for f in all_files: try: df pd.read_excel(f, engineopenpyxl) frames.append(df) except Exception as e: failed.append((f.name, str(e))) print(f跳过 {f.name}{e})跑完后检查failed列表有针对地处理那几个问题文件。这套容错机制在处理来源杂乱的批量数据时特别管用不会因为一个坏文件耽误整批任务。7. 把合并脚本沉淀成可复用的工具一次性脚本和可复用工具的区别就在于下次遇到类似需求时你要不要重写。我的做法是把合并逻辑封装成一个函数把文件夹路径、列名映射、输出路径作为参数传进去以后换个场景改几个参数就能用。def merge_excel_folder(folder, output, rename_mapNone, sheetNone): files list(Path(folder).glob(*.xlsx)) frames [] for f in files: df pd.read_excel(f, sheet_namesheet, engineopenpyxl) if rename_map: df.rename(columnsrename_map, inplaceTrue) df[来源文件] f.name frames.append(df) result pd.concat(frames, ignore_indexTrue, sortFalse) result.to_excel(output, indexFalse, engineopenpyxl) return result这个函数覆盖了八成以上的合并需求。需要处理多Sheet时把sheet传None需要改列名时传映射表其余情况直接调用。封装的好处不只是省事更重要的是逻辑固定下来之后不容易出错每次都是同一套经过验证的流程。我个人在实际操作中的体会是Excel合并这件事难点从来不在代码本身而在于数据源的规范程度和异常情况的处理。代码写熟了就那么几行真正花时间的是摸清每个文件的脾气、想清楚列怎么对齐、把脏数据挡在流程之外。把探测、校验、容错这三件事做到位你的合并脚本就能从“能跑”升级到“敢用”。后续如果数据量继续涨还可以把中间结果落到数据库里用SQL做合并那是另一个话题了等真到了那个量级再说。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进