news 2026/10/9 21:50:18

Python表格拼接合并实战:从手工复制到批量处理与性能优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python表格拼接合并实战:从手工复制到批量处理与性能优化

1. 从手工复制粘贴到代码批量拼接:为什么这件事值得认真对待

如果你日常工作中需要处理Excel,大概率遇到过这种场景:手头有十几个甚至几十个结构相同的表格文件,可能是各区域提交的月报、各门店的销售流水、各批次的产品检测记录,需要把它们汇总到一张总表里。手动操作的话,就是打开第一个文件、全选、复制、切到总表、粘贴、关掉、打开第二个……循环往复。文件少还能忍,一旦超过十个,不仅枯燥,还特别容易出错——漏粘一行、多粘一列、表头重复粘贴,这些坑几乎每个人都踩过。

用Python做表格拼接合并,解决的正是这个痛点。它的核心逻辑并不复杂:把多个来源的表格数据读取出来,按照统一的规则纵向堆叠或横向对接,最终输出一张完整的汇总表。听起来简单,但实际操作中会遇到各种细节问题:表头不一致怎么办?列顺序不同怎么处理?有的文件有合并单元格怎么读?数据量大了内存扛不住怎么办?这些才是真正决定代码能不能用在生产环境的关键。

这篇文章面向的是已经掌握Python基础语法、知道怎么用pandas读写Excel的读者。如果你还没接触过pandas,建议先花半小时了解一下DataFrame的基本概念,否则后面的内容可能会有些吃力。我会从最基础的纵向拼接讲起,逐步深入到多文件批量处理、列对齐、性能优化等实战环节,把每个操作背后的“为什么”讲清楚,让你不仅能抄代码,还能根据实际情况灵活调整。

2. 纵向拼接的三种实现路径与选型逻辑

纵向拼接是最常见的需求——多个表格的列结构相同,只是数据行不同,需要把它们上下叠在一起。pandas提供了不止一种方法来实现这个操作,不同方法适用于不同场景,选错了要么代码冗长,要么性能拉胯。

2.1 concat:最直接的堆叠方式

pd.concat()是pandas专门为拼接设计的函数,用法直观。假设你有两个DataFrame,列名完全一致,直接传入列表即可:

import pandas as pd df1 = pd.read_excel("区域A.xlsx") df2 = pd.read_excel("区域B.xlsx") result = pd.concat([df1, df2], axis=0, ignore_index=True)

这里有几个参数值得注意。axis=0表示纵向拼接,这也是默认值,但显式写出来可读性更好。ignore_index=True的作用是重置索引,如果不加这个参数,拼接后的DataFrame索引会是0,1,2...接着又是0,1,2...,后续如果要用索引定位数据就会很混乱。我个人的习惯是只要做拼接就加上这个参数,省得后面还要手动重置。

concat的一个优势是它支持一次拼接多个对象,不限于两个。你可以直接把一个包含十几个DataFrame的列表传进去,它会一次性完成所有拼接。这比写循环逐个拼接效率高得多,因为pandas在内部做了一些优化,减少了中间对象的创建。

2.2 append:已弃用的旧方法及其替代方案

早期版本的pandas提供了一个append方法,用法是df1.append(df2),看起来更简洁。但这个方法是逐个追加的,每次调用都会创建一个新的DataFrame对象,如果要在循环里拼接几十个文件,性能会非常差。pandas官方从1.4版本开始已经将append标记为弃用,未来版本会彻底移除。

如果你手头的旧代码还在用append,迁移方案很简单:把所有要拼接的DataFrame收集到一个列表里,最后用一次concat完成。这个改动不仅让代码更符合当前规范,在数据量大时性能提升也很明显。我实测过一个场景,拼接50个各含1万行的表格,用循环append耗时约12秒,改成列表收集加一次concat后降到不到2秒。

2.3 列表收集加批量拼接:推荐的标准写法

结合前两点的分析,处理多文件拼接的标准写法应该是这样的:

import pandas as pd from pathlib import Path folder = Path("./月度报表") all_data = [] for file_path in folder.glob("*.xlsx"): df = pd.read_excel(file_path) all_data.append(df) final = pd.concat(all_data, ignore_index=True)

这段代码的逻辑很清晰:遍历目标文件夹下所有xlsx文件,逐个读取后存入列表,最后一次性拼接。用pathlib.Path代替字符串拼接路径,跨平台兼容性更好,代码也更干净。

注意:如果文件夹里混有非数据文件(比如说明文档、临时文件),需要在循环里加判断条件过滤,否则读取时会报错中断。

选型建议总结成一句话:无论多少个表格,都先用列表收集,最后统一concat。这是目前最稳妥、性能最好的做法。

3. 多文件批量读取时绕不开的四个细节问题

把单个拼接的逻辑扩展到批量处理文件时,会遇到一些在单文件场景下不会暴露的问题。这些问题不解决,代码跑起来要么报错,要么结果不对。

3.1 表头行不一致:skiprows与header参数的配合

理想情况下,每个表格的第一行就是列名。但实际工作中,很多报表会在第一行放标题(比如“2024年第一季度销售统计”),第二行甚至第三行才是真正的表头。如果直接read_excel,pandas会把标题行当作列名,后续拼接时列名对不上,结果就是一堆Unnamed列。

处理方法是使用header参数指定表头所在的行号(从0开始计数)。如果表头在第三行,就写header=2。如果表头之前还有需要跳过的说明行,可以配合skiprows:

df = pd.read_excel(file_path, skiprows=1, header=0)

这表示跳过第一行,把接下来的第一行作为表头。实际操作中,我建议先单独读取一个文件,打印df.columns看看列名是否正确,确认无误后再批量处理。这个检查步骤花不了几秒钟,但能避免后面大量返工。

3.2 列顺序不同但列名相同:concat的自动对齐机制

concat在纵向拼接时,默认按照列名进行对齐,而不是按照列的位置。这意味着即使两个表格的列顺序不同,只要列名一致,拼接结果就是正确的。比如df1的列顺序是[日期, 销售额, 门店],df2是[门店, 日期, 销售额],concat之后会自动按列名匹配,不会出现数据错位。

但这个机制有一个副作用:如果某个表格缺少某一列,拼接后该列对应位置会填充NaN。这有时候是预期行为(确实没有这个数据),有时候则说明读取环节出了问题(比如列名有空格导致匹配失败)。所以拼接完成后,建议检查一下各列的缺失值比例:

print(final.isnull().sum())

如果发现某列大量缺失,就要回头排查是数据源本身的问题还是列名匹配的问题。

3.3 文件命名与遍历顺序:glob排序的坑

Path.glob()返回的文件顺序是不确定的,取决于文件系统的实现。如果你需要按照特定顺序拼接(比如按月份先后),不能依赖glob的默认顺序,需要手动排序:

files = sorted(folder.glob("*.xlsx"), key=lambda p: p.stem)

这样会按照文件名(不含扩展名)的字典序排列。如果文件名是“1月.xlsx”“2月.xlsx”这种格式,字典序恰好等于时间序。但如果是“1月”“10月”“2月”这种,字典序会把10月排在2月前面,需要额外处理。更稳妥的做法是在文件名里使用零填充的编号,比如“01月”“02月”……“12月”,这样字典序和时间序就一致了。

3.4 读取时的数据类型陷阱:数字变文本、日期变数字

Excel的一个特点是它不强制列的数据类型,同一列里可能混着数字和文本。pandas读取时会做类型推断,但推断结果不一定符合预期。常见的问题有两个:一是带前导零的编号(如“001”)被读成数字1,丢失了前导零;二是日期被读成数字序列号(Excel内部用数字存储日期)。

对于编号列,可以在读取时指定dtype参数:

df = pd.read_excel(file_path, dtype={"产品编号": str})

对于日期列,如果Excel里存储的是标准日期格式,pandas通常能正确识别。但如果显示为数字,说明该列的单元格格式不是日期,需要在Excel里先转换,或者读取后用pd.to_datetime配合origin参数手动转换。

4. 横向拼接与复杂场景:当简单堆叠不够用时

纵向拼接解决的是“结构相同、数据不同”的场景。但实际工作中还有另一类需求:多个表格的列不同,需要按照某个共同字段横向合并。这就是横向拼接,pandas里用merge或join来实现。

4.1 merge的核心参数:on、how、suffixes

merge的用法类似于SQL里的JOIN操作。假设你有一个订单表和一个客户信息表,需要通过客户ID关联:

orders = pd.read_excel("订单表.xlsx") customers = pd.read_excel("客户信息.xlsx") result = pd.merge(orders, customers, on="客户ID", how="left")

on参数指定关联键,两个表中这个列名必须一致。如果不一致,可以用left_on和right_on分别指定。how参数控制连接方式:left保留左表所有行,right保留右表所有行,inner只保留两表都有的行,outer保留所有行。选择哪种方式取决于业务逻辑——如果你要确保订单表的数据一条不漏,就用left。

suffixes参数处理列名冲突。如果两个表都有“备注”列,合并后pandas会自动加上后缀区分,默认是_x和_y。建议手动指定更有意义的后缀:

result = pd.merge(orders, customers, on="客户ID", how="left", suffixes=("_订单", "_客户"))

4.2 多对一与多对多:合并前必须搞清楚的关系

merge之前,一定要确认两个表的关联关系是一对一、多对一还是多对多。如果左表的关联键有重复值,右表也有重复值,合并结果会出现笛卡尔积——行数急剧膨胀。比如左表有3行同一个客户ID,右表有2行同一个客户ID,合并后这个客户会产生6行数据。

这不是bug,是merge的正常行为,但如果不了解这一点,看到结果行数暴增会一头雾水。合并前用duplicated()检查关联键的唯一性:

print(orders["客户ID"].duplicated().sum()) print(customers["客户ID"].duplicated().sum())

如果右表的关联键有重复,而你只想取其中一条,需要先去重或者做聚合。

4.3 拼接后的数据校验:行数、列数、关键字段核对

无论纵向还是横向拼接,完成后都应该做基本校验。纵向拼接检查总行数是否等于各表行数之和(在忽略表头重复的前提下);横向拼接检查行数是否符合预期(left连接应该等于左表行数)。列数方面,纵向拼接后列数应该等于各表列数的并集,横向拼接后列数等于两表列数之和减去关联键的重复计数。

关键字段核对是指抽查几行数据,确认拼接后的值与原表一致。我通常会在拼接后随机抽几行,用iloc定位到具体位置,和原始文件对照。这个步骤看起来笨,但能发现一些隐蔽的问题,比如编码问题导致的乱码、浮点数精度丢失等。

5. 性能优化:当表格大到内存装不下时怎么办

处理少量小文件时,上面的方法完全够用。但如果文件数量多、单个文件行数大,内存就会成为瓶颈。一个100万行、20列的DataFrame大约占用150MB内存,如果同时把50个这样的表读进列表再拼接,峰值内存可能超过7GB,普通办公电脑直接卡死。

5.1 分块读取与增量拼接

pandas的read_excel本身不支持分块读取(read_csv支持chunksize参数,但Excel没有)。不过我们可以换个思路:不把所有DataFrame都保存在内存里,而是边读边写。具体做法是先把第一个文件读进来写入结果文件,后续文件读一个追加一个。

import pandas as pd from pathlib import Path folder = Path("./大数据集") files = sorted(folder.glob("*.xlsx")) output = "汇总结果.xlsx" # 第一个文件写入,保留表头 first = pd.read_excel(files[0]) first.to_excel(output, index=False) # 后续文件追加,不写表头 for f in files[1:]: df = pd.read_excel(f) with pd.ExcelWriter(output, mode="a", if_sheet_exists="overlay") as writer: df.to_excel(writer, index=False, header=False, startrow=writer.sheets["Sheet1"].max_row)

这段代码利用了ExcelWriter的追加模式。需要注意的是,if_sheet_exists="overlay"参数要求pandas版本不低于1.4。另外,追加写入Excel的速度比一次性写入慢很多,因为每次都要打开和保存整个文件。如果数据量真的很大,建议中间结果用CSV格式暂存,最后再统一转成Excel。

5.2 只读取需要的列:usecols的妙用

很多情况下,我们并不需要表格里的所有列。比如一个20列的销售报表,汇总时只需要日期、门店、销售额三列。这时候用usecols参数只读取需要的列,能大幅减少内存占用和读取时间:

df = pd.read_excel(file_path, usecols=["日期", "门店", "销售额"])

实测下来,读取20列中的3列,速度大约是全列读取的40%,内存占用降到原来的15%左右。如果列名不确定,也可以传列号列表,比如usecols=[0, 3, 7]。但列号方式不够稳健,一旦源文件列顺序调整就会读错,所以优先用列名。

5.3 用CSV作为中间格式的取舍

Excel文件的读写速度远低于CSV。如果整个流程不需要保留Excel格式(比如只是做数据汇总分析),可以考虑先把所有Excel转成CSV,后续操作都在CSV上进行。转换是一次性成本,但后续的读取、拼接、筛选都会快很多。

for f in folder.glob("*.xlsx"): df = pd.read_excel(f) df.to_csv(f.with_suffix(".csv"), index=False)

之后用pd.read_csv读取,拼接完成后再输出为Excel。这个方案特别适合需要反复调试代码的场景——每次调试都重新读Excel太慢了,转成CSV后迭代速度会快很多。

6. 实战中积累的几个避坑经验

上面讲的都是方法论层面的东西,这一节分享几个我在实际项目中踩过的坑和总结的技巧,都是文档里不会写的。

第一个坑是文件被占用导致读取失败。如果某个Excel文件正在被其他程序打开(比如你刚双击看了一眼还没关),pandas读取时会抛出PermissionError。批量处理时遇到这种情况,整个循环就中断了。解决办法是用try-except包裹读取操作,记录失败的文件名,跳过继续:

failed = [] for f in files: try: df = pd.read_excel(f) all_data.append(df) except Exception as e: failed.append((f.name, str(e))) continue

处理完成后打印failed列表,手动处理这些文件。这个做法看起来简单,但在处理上百个文件时能省去大量重跑的时间。

第二个坑是隐藏的空行和空列。有些表格看起来数据到第100行结束,但实际上第101行有空格或不可见字符,pandas会把它当作有效数据读进来,导致拼接后多出很多空行。读取后可以用dropna(how="all")删除全空行:

df = df.dropna(how="all")

这个操作应该在拼接之前对每个DataFrame单独做,而不是拼接后统一做,因为拼接后的空行可能混在中间,不容易识别。

第三个经验是保留数据来源标记。拼接多个来源的数据时,建议在每读取一个文件后加一列记录来源:

df["来源文件"] = file_path.name

这样汇总后如果发现某行数据有问题,可以快速定位到是哪个文件贡献的。这一列在最终输出时可以保留也可以删除,但在调试阶段非常有用。

第四个经验关于输出时的格式控制。to_excel默认会把索引也写进去,通常我们不需要,记得加index=False。另外,如果数据里有长数字(比如身份证号、订单号),Excel打开后可能会显示为科学计数法。可以在写入时指定格式,或者干脆把这类列转成文本再写入。

7. 一套可直接复用的拼接脚本框架

把前面讲的内容整合起来,形成一个通用的脚本框架。这个框架覆盖了文件遍历、异常处理、列筛选、来源标记、拼接输出等环节,你可以根据自己的需求删减或扩展。

import pandas as pd from pathlib import Path import sys def merge_excel_files(folder_path, output_path, usecols=None, sheet_name=0): """ 批量拼接Excel文件 :param folder_path: 存放Excel文件的文件夹路径 :param output_path: 输出文件路径 :param usecols: 需要读取的列名列表,None表示全部读取 :param sheet_name: 工作表名称或索引 """ folder = Path(folder_path) files = sorted(folder.glob("*.xlsx")) if not files: print("未找到任何xlsx文件") return all_data = [] failed = [] for f in files: try: df = pd.read_excel(f, usecols=usecols, sheet_name=sheet_name) df = df.dropna(how="all") df["来源文件"] = f.name all_data.append(df) print(f"已读取: {f.name} ({len(df)}行)") except Exception as e: failed.append((f.name, str(e))) print(f"读取失败: {f.name} - {e}") if not all_data: print("没有成功读取任何文件") return result = pd.concat(all_data, ignore_index=True) result.to_excel(output_path, index=False) print(f"\n拼接完成: 共{len(result)}行, {len(result.columns)}列") print(f"输出文件: {output_path}") if failed: print(f"\n以下{len(failed)}个文件读取失败:") for name, err in failed: print(f" - {name}: {err}") if __name__ == "__main__": merge_excel_files( folder_path="./数据源", output_path="./汇总结果.xlsx", usecols=["日期", "门店", "销售额"] )

这个脚本可以直接运行,也可以作为模块导入。几个设计上的考虑:usecols参数让调用者决定读哪些列,避免读入无关数据;failed列表记录失败文件,方便事后排查;每读取一个文件打印进度,处理大量文件时能直观看到进展;来源文件列方便追溯数据。

如果需要在拼接后做进一步处理(比如按日期排序、按门店分组汇总),可以在concat之后、to_excel之前插入相应的代码。这个框架的价值在于把容易出错的环节都做了防护,你只需要关注业务逻辑本身。

实际使用中,我建议先用少量文件测试,确认输出结果符合预期后再处理全量数据。测试时重点检查列名是否正确、行数是否匹配、关键字段的值有没有异常。这几分钟的前置检查,往往能避免几小时的返工。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 21:48:20

Android音乐论坛APP源码实战:从ZIP导入到二次开发避坑指南

简介:基于Android技术的音乐论坛App源码包,面向Java方向毕业设计、课程设计以及希望实践移动端开发的大学生。项目采用Java后端与Vue/uni-app前端组合,同时包含微信小程序端(wxml/wxss)与后台管理页面,覆盖…

作者头像 李华
网站建设 2026/10/9 21:42:48

C#直读WinCC归档数据库:时间戳对齐与LZ77解压实战

简介:本资源是一套基于C#开发的WinCC归档数据库读取完整工程源码,面向工业自动化领域的.NET开发者、SCADA系统集成工程师及熟悉S7-300 PLC的现场技术人员,解决WinCC历史过程数据高效提取与二次分析的实际需求。压缩包共39个文件,含…

作者头像 李华
网站建设 2026/10/9 21:42:00

Python + Selenium + webdriver-manager 网页自动化截图实战

做网页自动化的人,早晚都会遇到一个需求:把网页当前的样子变成一张图片,留着存档、做巡检、发报告,或者仅仅是“眼见为实”。以前我接到这类需求,第一反应是requests拿页面源码,但很多页面是动态渲染出来的…

作者头像 李华
网站建设 2026/10/9 21:41:44

用PyQt5打造轻量级数据库操作工具:从QSqlTableModel到SQLite实战

简介:一份基于Python PyQt5开发的数据库操作小工具源码,同时附带了SQLite数据库文件,面向正在学习PyQt5界面编程与sqlite3数据库交互的开发者,尤其适合需要轻量级桌面数据库管理场景的动手实践。包内共171个文件,压缩包…

作者头像 李华
网站建设 2026/10/9 21:39:36

Godot引擎移植鸿蒙PC:跨生态适配的技术断层与分阶段实践

1. 项目概述:这不是一次简单的“移植”,而是一场跨生态的系统级适配Godot 游戏编辑器移植鸿蒙 PC——光看标题,很多人第一反应是“不就是换个平台编译一下?”但我在游戏引擎底层开发和跨平台工具链打磨上干了十多年,亲…

作者头像 李华
网站建设 2026/10/9 21:39:21

HiClaw 开源版本地安装:5 分钟跑通 OpenClaw 团队协作

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华