直接读千万行csv会爆内存,应采用流式分块(chunksize)、显式dtype、usecols筛选、parquet或sqlite增量落盘,并用pd.na保留缺失语义、切分文件避句柄耗尽。

用 pandas 直接读千万行 CSV 会爆内存?别这么干
直接调用 pandas.read_csv() 加载数千万行日志,大概率触发 MemoryError 或系统卡死——它默认把整张表载入内存构建 DataFrame。即使机器有 64GB 内存,日志字段多(比如含长 URL、JSON 片段)、编码不统一(如混合 UTF-8 和 GBK),实际内存占用常是原始文件的 3–5 倍。
真正可行的路径是「流式分块 + 类型预设 + 增量落盘」:
- 用
chunksize=100000参数分批读取,每块独立处理后立刻写入目标存储,不累积内存 - 务必通过
dtype显式指定列类型:字符串列用category(重复值多时省 70%+ 内存),数值列避免默认float64,改用int32或pd.Int64Dtype()(支持空值) - 跳过无用列:用
usecols只读取解析后需保留的字段,别让pandas白解析时间戳或冗余标记位
写入 SQLite 还是 Parquet?看你的查询模式
如果后续主要做「按时间范围查某服务错误码分布」这类 OLAP 查询,Parquet 是更优解;如果要频繁执行 SELECT * FROM logs WHERE ip = ? AND status > 400 这类带多条件、非时间主键的随机查询,SQLite 更直接。
关键差异点:
-
Parquet:用pyarrow引擎写入时开启use_dictionary=True(对日志中重复的level、service_name等列压缩极好),并按timestamp列分区(partition_cols=['year', 'month']),查询时可跳过 90% 文件 -
SQLite:建表时给高频查询字段加索引,例如CREATE INDEX idx_time_status ON logs(timestamp, status);但单库文件不宜超 2GB(WAL 模式下大事务易锁表),建议按天切分数据库文件
正则解析日志后字段缺失怎么办?别用 fillna() 硬填
日志格式不统一是常态:Nginx 日志里 $request_body 可能为空,Java 应用日志的 exception 字段只在报错时出现。用 df.fillna('') 或 df.fillna(0) 会污染数据语义——空字符串和“明确记录为空”是两回事。
正确做法是保留缺失语义:
- 字符串字段统一用
pd.NA(不是None或np.nan),配合stringdtype(pandas 1.0+) - 数值字段用
pd.Int64Dtype()或pd.BooleanDtype(),它们原生支持pd.NA - 解析时用
re.search()而非re.match(),避免因日志前缀差异(如 Docker 容器日志带时间戳前缀)导致整行字段全空
用 concurrent.futures 多进程加速解析?小心文件句柄耗尽
单进程解析 1000 万行可能要 8 分钟,起 4 个进程看似能缩到 2 分钟——但若每个进程都打开同一个日志文件,Linux 默认 ulimit -n 通常只有 1024,4 进程 × 每块 10 万行 × 缓冲区开销,极易触发 OSError: [Errno 24] Too many open files。
安全提速方案:
- 先用
split -l 1000000 big.log chunk_把原文件切分成小文件,再用ProcessPoolExecutor分配各 chunk 给进程,彻底规避共享文件句柄问题 - 每个子进程内仍用
chunksize=50000流式读,防止单块内存暴涨 - 禁用
pandas的infer_datetime_format=True(它会在首块尝试推断格式,失败后降级为慢解析),直接传parse_dates=['timestamp']和date_parser回调函数
最麻烦的从来不是解析速度,而是字段语义一致性——同一份日志里 user_id 有时是数字有时是字符串,有时为空白有时为 -,这种隐式约定必须在解析层就收口,不能指望下游修复。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











