爬虫数据量大了以后,CSV 和 JSON 有几个硬伤------文件太大、读写慢、不支持列式压缩。Parquet 是大数据生态中最主流的列式存储格式,文件体积小、读写速度快、自带 schema,非常适合存储大规模爬虫数据。
一、CSV vs JSON vs Parquet
| 对比 | CSV | JSON | Parquet |
|---|---|---|---|
| 100万行体积 | ~80MB | ~120MB | ~15MB 🏆 |
| 读写速度 | 慢(全表扫描) | 慢(全量解析) | 快(列式读取) 🏆 |
| 类型支持 | 全是字符串 | 部分支持 | 完整支持 🏆 |
| 压缩率 | 一般 | 差 | 极高(Snappy/ZSTD) 🏆 |
| 可读性 | 好 | 好 | 二进制,不可直接看 |
简单说: 爬几百条数据用 CSV/JSON 没问题。每天爬几万条以上,上 Parquet 能省 80% 的磁盘空间。
二、安装
bash
pip install pandas pyarrow # pyarrow 提供 Parquet 支持
三、写入 Parquet
python
import pandas as pd
import os
# 1. 爬虫数据(DataFrame)
df = pd.DataFrame({
"title": ["iPhone 15", "华为 Mate 60", "小米 14"],
"price": [6999, 5999, 3999],
"brand": ["Apple", "华为", "小米"],
"stock": [100, 50, 200],
"crawl_time": pd.Timestamp.now(),
})
# 2. 保存为 Parquet
df.to_parquet(
"products.parquet",
engine="pyarrow",
compression="snappy", # 压缩算法,速度和压缩比的平衡
index=False,
)
# 查看文件大小
size = os.path.getsize("products.parquet")
print(f"Parquet 文件大小: {size / 1024:.1f} KB")
# CSV 对比(同样数据)
df.to_csv("products.csv", index=False)
csv_size = os.path.getsize("products.csv")
print(f"CSV 文件大小: {csv_size / 1024:.1f} KB")
四、读取 Parquet
python
# 读取
df = pd.read_parquet("products.parquet", engine="pyarrow")
print(df.head())
# 只读取指定列(列式存储的优势)
df = pd.read_parquet("products.parquet", columns=["title", "price"])
print(df.head())
五、增量写入------每天追加新数据
爬虫每天跑一次,需要把新数据追加到已有的 Parquet 文件中。Parquet 本身不可变,但可以用分区 或合并的方式实现增量。
1. 按天分区(推荐)
python
from datetime import datetime
import os
def save_daily_data(df, base_dir="crawled_data"):
"""
按天分区存储,每天一个 Parquet 文件
这样每天只需写入当天的文件,不需要合并历史数据
"""
today = datetime.now().strftime("%Y-%m-%d")
day_dir = os.path.join(base_dir, today)
os.makedirs(day_dir, exist_ok=True)
file_path = os.path.join(day_dir, "data.parquet")
df.to_parquet(file_path, engine="pyarrow", compression="snappy", index=False)
print(f"已保存: {file_path} ({len(df)} 条)")
# 使用
df = crawl_today_data()
save_daily_data(df)
目录结构:
crawled_data/
├── 2026-06-01/
│ └── data.parquet
├── 2026-06-02/
│ └── data.parquet
├── 2026-06-03/
│ └── data.parquet
└── ...
2. 合并到历史文件
如果不想分区,可以每天合并到同一个大文件:
python
def append_to_parquet(new_df, file_path):
"""
增量追加到 Parquet 文件
"""
if os.path.exists(file_path):
# 1. 读取历史数据
old_df = pd.read_parquet(file_path)
# 2. 合并
combined = pd.concat([old_df, new_df], ignore_index=True)
# 3. 去重(按标题去重,保留最新的)
combined = combined.drop_duplicates(subset=["title"], keep="last")
else:
combined = new_df
# 4. 写回
combined.to_parquet(file_path, engine="pyarrow", compression="snappy", index=False)
print(f"写入完成,共 {len(combined)} 条 (新增 {len(new_df)} 条)")
3. 生产环境推荐方案
python
import pandas as pd
import os
from datetime import datetime
import hashlib
class ParquetCrawlerStorage:
"""基于 Parquet 的爬虫存储(增量写入 + 去重)"""
def __init__(self, base_dir="crawled_data"):
self.base_dir = base_dir
os.makedirs(base_dir, exist_ok=True)
self.file_path = os.path.join(base_dir, "all_data.parquet")
def save_batch(self, items: list[dict]):
"""批量保存爬虫数据"""
new_df = pd.DataFrame(items)
# 添加爬取时间
new_df["crawl_time"] = datetime.now()
# 添加内容指纹(用于去重)
new_df["_fingerprint"] = new_df.apply(
lambda row: hashlib.md5(
str(row.to_dict()).encode()
).hexdigest(),
axis=1
)
# 增量合并
if os.path.exists(self.file_path):
old_df = pd.read_parquet(self.file_path)
df = pd.concat([old_df, new_df], ignore_index=True)
# 按指纹去重,保留最新
df = df.drop_duplicates(subset=["_fingerprint"], keep="last")
else:
df = new_df
# 保存(去掉指纹列)
df = df.drop(columns=["_fingerprint"])
df.to_parquet(self.file_path, engine="pyarrow", compression="snappy", index=False)
return len(new_df)
def query(self, **filters):
"""查询数据"""
df = pd.read_parquet(self.file_path)
for col, val in filters.items():
if col in df.columns:
df = df[df[col] == val]
return df
def stats(self):
"""数据统计"""
if not os.path.exists(self.file_path):
return {"total": 0}
df = pd.read_parquet(self.file_path)
return {
"total": len(df),
"columns": list(df.columns),
"file_size_mb": os.path.getsize(self.file_path) / 1024 / 1024,
}
# 使用
storage = ParquetCrawlerStorage("products_data")
# 每天爬完调用
items = [{"title": "iPhone 15", "price": 6999}]
storage.save_batch(items)
# 查询
df = storage.query(brand="Apple")
print(df.head())
# 查看统计
print(storage.stats())
六、Parquet 文件读取性能
python
import time
# 假设有一个 500MB 的 CSV 文件和一个同数据的 Parquet 文件
# CSV:全量读取
start = time.time()
df_csv = pd.read_csv("large_data.csv")
print(f"CSV 读取: {time.time() - start:.2f}s")
# Parquet:全量读取
start = time.time()
df_pq = pd.read_parquet("large_data.parquet")
print(f"Parquet 读取: {time.time() - start:.2f}s")
# Parquet:只读两列(列式剪枝,速度更快)
start = time.time()
df_pq_part = pd.read_parquet("large_data.parquet", columns=["title", "price"])
print(f"Parquet 部分读取: {time.time() - start:.2f}s")
百万行数据测试结果参考:
CSV 读取: 12.5s
Parquet 全量: 2.3s
Parquet 部分列: 0.2s
七、Parquet 与数据分析工具
python
# 按品牌分组统计
df = pd.read_parquet("products.parquet")
print(df.groupby("brand")["price"].agg(["count", "mean", "max"]))
# 存储空间对比
# 原始 DataFrame → CSV: 82MB
# → Parquet(snappy): 15MB (缩小 82%)
# → Parquet(zstd): 12MB (缩小 85%)
八、什么时候用 Parquet
| 数据量 | 推荐格式 | 原因 |
|---|---|---|
| < 1万条 | CSV/JSON | 简单,肉眼可读 |
| 1万~10万 | CSV 或 Parquet | 两种都可,看习惯 |
| 10万~100万 | Parquet | 体积小 80%,读写快 5 倍 |
| > 100万 | Parquet + 分区 | 列式存储 + 分区裁剪 |
总结
CSV → 小数据量,直接看
JSON → 接口数据交换
Parquet → 大规模存储和分析
如果数据量已经上万,建议转到 Parquet。代码改动很小(to_csv 改成 to_parquet),省下的磁盘和读取时间非常可观。
💡 觉得有用的话,点赞 + 关注【张老师技术栈】吧!每周更新 Java/Python/爬虫 实战干货,不让你白来。