You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python拆分超大文件为小文件:打开文件过多问题

这种1TB级大CSV拆分的坑我踩过不少——要么频繁开关文件导致IO慢到离谱,要么一次性开太多文件触发系统句柄限制,确实两难。结合你遇到的问题,给你几个经过生产环境验证的优化方案:

方案1:文件句柄池(复用+LRU淘汰)

核心思路是维护一个固定大小的活跃文件句柄池,不一次性打开所有目标文件,而是动态复用:当需要写入某个文件时,如果它已经在池里就直接用;不在的话,关闭池里最久没被使用的文件,再打开新的加入池。这样既避免了频繁开关文件的开销,又把同时打开的文件数控制在系统能承受的范围(比如50-100个)。

举个Python实现的例子,用OrderedDict来跟踪文件的使用顺序,实现LRU淘汰:

from collections import OrderedDict

class FileHandlePool:
    def __init__(self, max_handles=50):
        self.max_handles = max_handles
        self.handles = OrderedDict()  # 有序字典,记录使用顺序

    def get_handle(self, filename):
        if filename in self.handles:
            # 移到末尾标记为最近使用
            self.handles.move_to_end(filename)
            return self.handles[filename]
        # 超过上限,关闭最久未使用的
        if len(self.handles) >= self.max_handles:
            oldest_filename, oldest_handle = self.handles.popitem(last=False)
            oldest_handle.close()
        # 打开新文件并加入池(用1MB缓冲区减少IO)
        handle = open(filename, 'a', encoding='utf-8', buffering=1024*1024)
        self.handles[filename] = handle
        return handle

    def close_all(self):
        for handle in self.handles.values():
            handle.close()
        self.handles.clear()

# 使用示例
pool = FileHandlePool(max_handles=50)
with open('large_file.csv', 'r', encoding='utf-8', buffering=1024*1024) as src:
    header = src.readline()
    # 提前给所有目标文件写入表头(如果需要,需先获取所有目标文件名列表)
    # for fn in target_filenames:
    #     with open(fn, 'w', encoding='utf-8') as f:
    #         f.write(header)
    for line in src:
        # 替换成你的分组逻辑,这里假设按第二列分组
        group_key = line.split(',')[1].strip()
        target_filename = f'{group_key}.csv'
        handle = pool.get_handle(target_filename)
        handle.write(line)
pool.close_all()

方案2:批量分组缓存写入

如果系统句柄限制特别严格,或者分组键数量极多,可以用内存缓存+批量写入的方式:先把同一分组的行攒到内存里,攒到一定数量(比如1000行)再一次性写入文件,写完就关闭。这样既减少了文件打开/关闭的次数,又不会占用过多内存。

示例代码:

from collections import defaultdict

buffer_size = 1000  # 每个分组攒够1000行再写入
buffer = defaultdict(list)

with open('large_file.csv', 'r', encoding='utf-8', buffering=1024*1024) as src:
    header = src.readline()
    seen_groups = set()
    for line in src:
        # 替换成你的分组逻辑
        group_key = line.split(',')[1].strip()
        target_filename = f'{group_key}.csv'
        # 第一次遇到分组时写入表头
        if group_key not in seen_groups:
            with open(target_filename, 'w', encoding='utf-8') as f:
                f.write(header)
            seen_groups.add(group_key)
        # 加入缓存
        buffer[group_key].append(line)
        # 达到缓存阈值,批量写入
        if len(buffer[group_key]) >= buffer_size:
            with open(target_filename, 'a', encoding='utf-8') as f:
                f.writelines(buffer[group_key])
            buffer[group_key].clear()
    # 处理剩余未写入的行
    for group_key, lines in buffer.items():
        target_filename = f'{group_key}.csv'
        with open(target_filename, 'a', encoding='utf-8') as f:
            f.writelines(lines)

方案3:用系统级工具快速拆分(适合简单分组)

如果你的分组逻辑比较简单(比如按某一列的值拆分),可以直接用Linux/macOS下的awk命令,它处理大文件的效率比Python高得多,而且内部会自动优化文件句柄的使用:

# 按第一列拆分,自动处理文件句柄,每10000行清理一次旧句柄
awk -F ',' '
    NR==1 {header=$0; next}
    {
        filename = $1 ".csv"
        if (!seen[filename]) {
            print header > filename
            seen[filename] = 1
        }
        print > filename
        # 每处理10000行关闭一次旧文件,避免句柄耗尽
        if (NR % 10000 == 0) {
            for (f in seen) close(f)
        }
    }
' large_file.csv

额外注意事项

  • 备份原文件:操作前一定要备份,避免拆分过程中数据损坏;
  • 缓冲区优化:无论是Python还是系统命令,都尽量使用较大的缓冲区(比如1MB以上),减少磁盘IO次数;
  • 极端分组场景:如果分组键数量达到百万级以上,建议先对分组键做哈希取模,先拆成几十个大批次,再每个批次内细分,避免内存溢出或文件管理混乱。

内容的提问来源于stack exchange,提问作者Thomas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 09:08:31