引言

上周接到一个需求,要处理一个 12GB 的用户行为日志 CSV 文件。一开始我想当然地用了pd.read_csv(),结果电脑直接卡死,内存飙升到 99%,强制重启后还丢了半天的工作。

后来花了整整两天时间,试了 N 种方法,踩了无数坑,终于找到了一套稳定高效的处理方案。今天把这些经验分享出来,希望能帮到同样遇到大文件处理问题的同学。

坑 1:直接用 pandas.read_csv () 一次性加载

这是最容易犯的错误。pandas 默认会把整个文件加载到内存中,对于超过可用内存的文件,直接 OOM(内存不足)。

错误代码:

python

运行

import pandas as pd

# 12GB文件,直接GG
df = pd.read_csv("user_behavior_12gb.csv")

解决方案:分块读取

python

运行

# 每次只读取10万行
chunk_size = 100000
chunks = pd.read_csv("user_behavior_12gb.csv", chunksize=chunk_size)

for i, chunk in enumerate(chunks):
    print(f"处理第{i+1}块,共{len(chunk)}行")
    # 在这里处理每一块数据
    process_chunk(chunk)

坑 2:分块读取但没有指定数据类型

pandas 会自动推断数据类型,这不仅慢,还会占用更多内存。比如一个全是整数的列,可能会被推断为 float 类型,内存占用翻倍。

优化方案:显式指定 dtype

python

运行

dtype_dict = {
    "user_id": "int32",
    "item_id": "int32",
    "behavior_type": "category",
    "timestamp": "int64"
}

chunks = pd.read_csv(
    "user_behavior_12gb.csv",
    chunksize=chunk_size,
    dtype=dtype_dict,
    usecols=["user_id", "item_id", "behavior_type"]  # 只读取需要的列
)

坑 3:使用 list.append () 累积结果

很多人会在循环中用 list.append () 来收集每块的处理结果,当结果量很大时,这同样会导致内存爆炸。

解决方案:边处理边写入

python

运行

# 不要这样做
# results = []
# for chunk in chunks:
#     results.append(process_chunk(chunk))
# final_result = pd.concat(results)

# 正确做法:边处理边写入文件
with open("result.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["user_id", "click_count"])
    
    for chunk in chunks:
        chunk_result = process_chunk(chunk)
        writer.writerows(chunk_result.values)

坑 4:忽略了 CSV 文件的编码问题

很多时候大文件是 GBK 编码或者其他编码,直接用默认的 utf-8 读取会报错,而且错误信息很不友好。

解决方案:先检测编码,再读取

python

运行

import chardet

# 先读取一小部分检测编码
with open("user_behavior_12gb.csv", "rb") as f:
    raw_data = f.read(10000)
    result = chardet.detect(raw_data)
    encoding = result["encoding"]
    print(f"检测到文件编码:{encoding}")

# 然后用检测到的编码读取
chunks = pd.read_csv("user_behavior_12gb.csv", chunksize=chunk_size, encoding=encoding)

坑 5:没有使用更高效的文件格式

CSV 是文本格式,读写都很慢,而且占用空间大。如果需要多次处理同一个文件,强烈建议转换成更高效的二进制格式。

终极优化:转换为 Parquet 格式

python

运行

# 第一次处理时转换
for i, chunk in enumerate(chunks):
    chunk.to_parquet(f"temp/chunk_{i}.parquet", index=False)

# 后续处理直接读取Parquet文件,速度快10倍以上
import pyarrow.parquet as pq

table = pq.read_table("temp/")
df = table.to_pandas()

总结

处理大文件的核心思想就是 "分而治之" 和 "按需加载"。记住这几个原则:

  1. 永远不要一次性加载超过可用内存的数据
  2. 显式指定数据类型,只读取需要的列
  3. 边处理边写入,不要在内存中累积结果
  4. 优先使用二进制格式存储数据

最后给大家一个小建议:如果经常需要处理 GB 级别的数据,还是升级一下电脑内存吧,16G 是底线,32G 会舒服很多。

更多推荐