直接用pandas.to_parquet()会爆内存,因其默认不分块、不压缩、一次性加载全量DataFrame序列化;应改用PyArrow手动分批写入,设置row_group_size、compression及schema类型强转。

为什么直接用 pandas.to_parquet() 会爆内存?
爬虫数据常含大量文本字段(如 HTML 片段、JSON 字符串)、稀疏列或变长嵌套结构,pandas.to_parquet() 默认不压缩、不分块,一次性加载全量 DataFrame 再序列化,极易触发 MemoryError 或 OOM Kill。
关键问题不在 Parquet 本身,而在 pandas 的写入策略:它默认把整个 DataFrame 当作一个逻辑块处理,而 PyArrow 或 fastparquet 后端并不自动做行组切分或字典编码优化。
- 务必设置
engine='pyarrow'(fastparquet 对复杂 schema 支持弱,尤其含 null 的 string 列) - 强制启用压缩:
compression='snappy'(平衡速度与体积;'zstd'更省空间但 CPU 高) - 显式控制行组大小:
row_group_size=100_000(避免单个 row group > 500MB,影响读取局部性) - 对高基数字符串列(如 URL、title),提前转为
category类型,触发 PyArrow 字典编码
如何边爬边存、不落地中间 CSV?
不要先 collect 所有数据到 list 再转 DataFrame —— 这等于把内存压力推迟到写入前一刻。应使用生成器 + 分批写入。
核心是用 pyarrow.parquet.ParquetWriter 手动管理文件句柄和 row group:
立即学习“Python免费学习笔记(深入)”;
import pyarrow as pa
import pyarrow.parquet as pq
<p>schema = pa.schema([
('url', pa.string()),
('status', pa.int32()),
('html_len', pa.int64()),
('fetched_at', pa.timestamp('ms')),
])</p><p>writer = pq.ParquetWriter(
'crawl_data.parquet',
schema=schema,
compression='snappy',
use_dictionary=True, # 对 string 自动建字典
write_batch_size=10_000,
)</p><p>for batch in crawl_generator(): # 每次 yield 一个 pd.DataFrame 或 pa.Table
table = pa.Table.from_pandas(batch, schema=schema)
writer.write_table(table)</p><p>writer.close()
注意:crawl_generator() 必须控制每批 size 在 1–5 万行之间;太大仍会内存抖动,太小则 I/O 开销上升。
字段类型不匹配导致读取出错怎么办?
爬虫数据天然脏:同一字段可能某次是 None,某次是 'N/A',某次是数字字符串。pandas 推断类型不稳定,写入 Parquet 后再读可能报 ArrowInvalid: Unable to cast from string to int64。
- 写入前统一强转:
df['status'] = pd.to_numeric(df['status'], errors='coerce')(转为 float64,null 变 NaN) - 字符串列一律用
df[col] = df[col].astype('string')(pandas 1.0+ 的 nullable string dtype) - 时间字段必须转
datetime64[ns]并设 timezone(如dt.tz_localize('UTC')),否则 Parquet 元数据里时区信息丢失 - 避免用
objectdtype 存混合类型;宁可拆成多列(如meta_json存原始字符串,meta_parsed存解析后结构)
分区写入后怎么避免“小文件灾难”?
按日期或域名分区(如 dt=2024-04-01/domain=example.com/)能加速查询,但爬虫流量不均会导致某些分区只有几 KB,另一些几百 MB —— 小文件过多会让 Spark/Flink 任务启动变慢,HDFS NameNode 压力大。
解决思路不是禁止分区,而是控制最小分区粒度 + 合并策略:
- 分区键选低基数、高稳定字段,比如
dt(日级)比hour更稳妥 - 写入时用
pyarrow.dataset.write_dataset()替代单文件写,它内置了max_open_files和file_visitor回调,可动态合并小批次 - 上线后定期用
ds = pq.ParquetDataset(...); ds.files扫描,对 ParquetFile.merge_pieces() 合并
真正难的不是写进去,是让后续分析引擎能高效定位、跳过无效 row group —— 这依赖正确的统计信息(use_dictionary=True、write_statistics=True)和合理 row group size。别为了省事关掉统计。


















