Python中高效拼接数千个DataFrame的最优方法是什么?
你当前写法运行越来越慢的根本原因,是把concat合并逻辑放到了循环内部:
每读一个小CSV,就会把之前已经累计的所有DataFrame和新读的文件重新拼接一次,而pandas的concat每次执行都会重新申请连续内存,把全量已有数据完整拷贝一遍。30000次循环下来,光冗余的数据拷贝操作就占了90%以上的运行时间,整体时间复杂度是O(n²),自然越到后面跑得越慢。
方案1:最小改动的快速优化(90%的场景适用)
只需要把原来循环里反复合并的逻辑改掉,先把所有读到的DataFrame存在一个列表里,等所有文件都读完了,最后一次性做一次concat就行。整个流程只需要做1次全量内存分配和数据拷贝,相比原来的写法,性能能提升10-20倍,改起来只需要动几行代码。
示例代码:
# 初始化空列表,专门存读到的每个小DataFrame df_pool = [] for file in onlyfiles: # 读文件时可以加两个实用参数,进一步提速、降内存 # 1. usecols:只读你需要的两列,不用加载CSV里其他无关的列 # 2. dtype:提前指定列的数据类型,避免pandas自动推断类型带来的额外开销 current_df = pd.read_csv( file, usecols=["product_id", "price"], dtype={"product_id": str, "price": "float32"} ) df_pool.append(current_df) # 所有文件都读完了,最后一次性合并 final_df = pd.concat(df_pool, ignore_index=True, copy=False)
小提示:因为所有CSV都是你自己爬的,列结构肯定是统一的,加
usecols和dtype这两个参数,读文件的速度还能再提30%左右,内存占用也会降一半以上。
方案2:低内存占用的无全量加载方案
如果30000个CSV总大小特别大(比如总共有几十GB),你的设备可用内存扛不住把所有DataFrame都存在列表里,可以直接跳过内存合并的步骤,逐文件往最终的合并CSV里追加内容,全程同一时间只需要加载单个小文件的内容,内存占用几乎可以忽略,哪怕10万个文件也不会爆内存。
示例代码:
merged_output = "all_products_merged.csv" for idx, file in enumerate(onlyfiles): try: current_df = pd.read_csv(file) # 第一个文件正常写,带上表头 if idx == 0: current_df.to_csv(merged_output, index=False, mode="w") # 从第二个文件开始,跳过表头,用追加模式写进去 else: current_df.to_csv(merged_output, index=False, mode="a", header=False) except Exception as e: # 遇到空文件、损坏的坏文件直接跳过,记录下来就行,不要让整个循环崩掉 print(f"读取{file}失败,已跳过")
方案3:追求极致速度的并行读取方案
合并大量小CSV的操作本质是磁盘IO密集型任务,单线程读文件没法把SSD/机械硬盘的读写带宽吃满,可以用多进程并行读文件,再把结果汇总合并,相比单线程读取还能再快2-5倍。
实现逻辑很简单:开和磁盘IO能力匹配的固定数量进程(一般开4-8个进程就够,不要开太多挤占内存),每个进程负责读取一个CSV并返回DataFrame,等所有进程都把文件读完,主线程再一次性执行concat汇总结果就行。对新手来说如果觉得配置多进程麻烦,用前两个方案基本就足够覆盖30000个文件的合并需求了。
- 千万不要在循环里反复写
df = pd.concat([df, new_df]),这是合并大量小文件时最慢的写法,没有之一 - 如果不需要保留原来每个小文件的行索引,
concat的时候一定要加ignore_index=True,避免pandas做多余的索引对齐计算 - 读文件的时候尽量用
dtype指定列类型,不要让pandas自动推断类型,否则3万个文件的类型推断开销也会占不少时间
内容的提问来源于stack exchange,提问作者Jauhnax

