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

超大CSV文件列唯一值统计的内存溢出问题及解决方案

解决超大CSV文件统计唯一值的内存溢出问题

你的代码在小文件上运行正常,但处理20GB以上超大文件时出现内存不足、速度变慢的核心原因是把所有列的非空值及其计数全存在内存里(就是代码里的column_data嵌套字典)。当列的唯一值数量达到百万甚至亿级时,内存会被快速耗尽,频繁的垃圾回收拖慢速度,最终触发内存溢出错误。下面给你几个实用的解决办法:

一、用SQLite替代内存存储(精确统计)

把唯一值的计数放到磁盘上的SQLite数据库中,内存只需要处理当前行的数据,完全不用担心内存不够。SQLite轻量高效,处理这种键值更新的性能足够支撑超大文件处理。

修改后的代码示例:

import csv
import sqlite3
from contextlib import closing

def read_large_csv(file_path, delimiter, encoding):
    # 连接磁盘上的SQLite数据库(避免内存数据库重启丢失数据)
    with closing(sqlite3.connect('csv_stats.db')) as conn:
        cursor = conn.cursor()
        # 创建表存储各列的唯一值计数,联合主键确保列+值唯一
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS column_values (
                column_name TEXT, 
                value TEXT, 
                count INTEGER, 
                PRIMARY KEY (column_name, value)
            )
        ''')
        
        with open(file_path, encoding=encoding) as f:
            reader = csv.reader(f, delimiter=delimiter)
            header = next(reader)
            # 初始化基础统计项
            columns = {
                col_name: {'total': 0, 'not_null': 0, 'unique': 0, 'first_five': set()} 
                for col_name in header
            }
            num = 0
            
            for row in reader:
                if num % 1000000 == 0:
                    print(f"{num / 1000000} M rows processed")
                num += 1
                
                for col_index, col_value in enumerate(row):
                    col_name = header[col_index]
                    columns[col_name]['total'] += 1
                    
                    if col_value:
                        columns[col_name]['not_null'] += 1
                        # 插入或更新计数:存在则+1,不存在则初始化为1
                        cursor.execute('''
                            INSERT OR REPLACE INTO column_values 
                            (column_name, value, count) 
                            VALUES (?, ?, COALESCE((SELECT count FROM column_values WHERE column_name=? AND value=?), 0) + 1)
                        ''', (col_name, col_value, col_name, col_value))
                        # 检查是否是首次出现的新值
                        cursor.execute('SELECT count FROM column_values WHERE column_name=? AND value=?', 
                                      (col_name, col_value))
                        count = cursor.fetchone()[0]
                        if count == 1:
                            columns[col_name]['unique'] += 1
                            if len(columns[col_name]['first_five']) < 5:
                                columns[col_name]['first_five'].add(col_value)
                # 每处理1000行提交一次,避免频繁IO
                if num % 1000 == 0:
                    conn.commit()
            # 提交剩余操作
            conn.commit()
            
            # 输出统计结果
            for col_name, stats in columns.items():
                print(f"列 {col_name}:")
                print(f"  总行数: {stats['total']}")
                print(f"  非空值数: {stats['not_null']}")
                print(f"  唯一值数: {stats['unique']}")
                print(f"  前5个唯一值: {stats['first_five']}")

二、用HyperLogLog做近似统计(内存占用极低)

如果业务可以接受1%左右的误差,HyperLogLog算法是最优解。它只需要几KB到几十KB的内存,就能快速估算出百万甚至亿级别的唯一值数量,完全不会出现内存问题。

先安装依赖库:pip install hyperloglog

代码示例:

import csv
from hyperloglog import HyperLogLog

def read_large_csv_approx(file_path, delimiter, encoding):
    with open(file_path, encoding=encoding) as f:
        reader = csv.reader(f, delimiter=delimiter)
        header = next(reader)
        # 每个列初始化一个HyperLogLog实例(0.01代表误差率)
        hll_instances = {col_name: HyperLogLog(0.01) for col_name in header}
        columns = {
            col_name: {'total': 0, 'not_null': 0, 'first_five': set()} 
            for col_name in header
        }
        num = 0
        
        for row in reader:
            if num % 1000000 == 0:
                print(f"{num / 1000000} M rows processed")
            num += 1
            
            for col_index, col_value in enumerate(row):
                col_name = header[col_index]
                columns[col_name]['total'] += 1
                
                if col_value:
                    columns[col_name]['not_null'] += 1
                    # 添加值到HyperLogLog(需要转成字节)
                    hll_instances[col_name].add(col_value.encode('utf-8'))
                    # 记录前5个唯一值
                    if len(columns[col_name]['first_five']) < 5:
                        columns[col_name]['first_five'].add(col_value)
        
        # 输出近似统计结果
        for col_name, stats in columns.items():
            approx_unique = hll_instances[col_name].cardinality()
            print(f"列 {col_name}:")
            print(f"  总行数: {stats['total']}")
            print(f"  非空值数: {stats['not_null']}")
            print(f"  近似唯一值数: {approx_unique}")
            print(f"  前5个唯一值: {stats['first_five']}")

三、分块处理+合并结果(精确统计,适合200GB级文件)

把超大文件分割成多个小文件(比如每个1GB对应约百万行),逐个处理每个小文件得到该文件的唯一值集合,最后合并所有小文件的结果去重,得到总数。这样每个小文件处理时内存可控,完全适配超大规模文件。

代码示例(分割+处理+合并):

import csv
import os
from collections import defaultdict

def split_large_csv(file_path, chunk_size=10**6, encoding='utf-8'):
    """把大文件分割成每个chunk_size行的小文件"""
    chunk_num = 0
    with open(file_path, encoding=encoding) as f:
        reader = csv.reader(f)
        header = next(reader)
        while True:
            chunk_name = f"chunk_{chunk_num}.csv"
            with open(chunk_name, 'w', encoding=encoding, newline='') as chunk_f:
                writer = csv.writer(chunk_f)
                writer.writerow(header)
                # 写入chunk_size行数据
                for _ in range(chunk_size):
                    try:
                        row = next(reader)
                        writer.writerow(row)
                    except StopIteration:
                        chunk_num +=1
                        return chunk_num
                chunk_num +=1

def process_chunk(chunk_path, encoding='utf-8'):
    """处理单个小文件,返回该文件的统计结果"""
    with open(chunk_path, encoding=encoding) as f:
        reader = csv.reader(f)
        header = next(reader)
        chunk_stats = {
            col: {'not_null':0, 'unique': set()} 
            for col in header
        }
        total_rows = 0
        
        for row in reader:
            total_rows +=1
            for idx, val in enumerate(row):
                col_name = header[idx]
                if val:
                    chunk_stats[col_name]['not_null'] +=1
                    chunk_stats[col_name]['unique'].add(val)
        
        return total_rows, chunk_stats

def merge_results(all_chunk_results):
    """合并所有小文件的统计结果"""
    merged = {}
    total_total_rows = 0
    for total_rows, chunk_data in all_chunk_results:
        total_total_rows += total_rows
        for col_name, stats in chunk_data.items():
            if col_name not in merged:
                merged[col_name] = {'total':0, 'not_null':0, 'unique': set()}
            merged[col_name]['total'] += total_rows
            merged[col_name]['not_null'] += stats['not_null']
            merged[col_name]['unique'].update(stats['unique'])
    
    # 把唯一值集合转为数量
    for col in merged:
        merged[col]['unique'] = len(merged[col]['unique'])
    merged['total_rows'] = total_total_rows
    return merged

# 使用示例
if __name__ == "__main__":
    file_path = "huge_file.csv"
    delimiter = ","
    encoding = "utf-8"
    
    print("分割大文件...")
    chunk_count = split_large_csv(file_path, encoding=encoding)
    
    print("处理每个小文件...")
    all_results = []
    for i in range(chunk_count):
        chunk_path = f"chunk_{i}.csv"
        print(f"处理第 {i+1}/{chunk_count} 个小文件")
        res = process_chunk(chunk_path, encoding=encoding)
        all_results.append(res)
        # 处理完删除小文件节省磁盘空间
        os.remove(chunk_path)
    
    print("合并统计结果...")
    final_stats = merge_results(all_results)
    
    # 输出最终结果
    print(f"总行数: {final_stats['total_rows']}")
    for col_name, stats in final_stats.items():
        if col_name == 'total_rows':
            continue
        print(f"列 {col_name}:")
        print(f"  非空值数: {stats['not_null']}")
        print(f"  唯一值数: {stats['unique']}")

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 23:52:03