把 Python 对象写到文件或传给另一个服务,选错序列化格式,后果比写错代码还烦——磁盘占用翻倍、跨语言对接卡壳、甚至 pickle 反序列化直接被注入恶意代码。五种主流格式各有明确适用场景,这篇把它们的边界和用法一次讲清。
五种格式的核心差异
先看一张对比表,心里有个锚:
| 格式 | 人类可读 | 跨语言 | 典型体积 | 适合场景 |
|---|---|---|---|---|
| JSON | ✅ | ✅ | 中 | API 交互、配置文件、前端对接 |
| CSV | ✅ | ✅ | 小(纯表格) | 表格数据导出、Excel/BI 导入 |
| Pickle | ❌ | ❌(仅 Python) | 大 | Python 内部缓存、短期持久化 |
| Parquet | ❌ | ✅(生态广) | 小(列存压缩) | 大规模分析、Spark / Pandas 批处理 |
| Protobuf | ❌ | ✅(多语言 SDK) | 小 | 高频 RPC、微服务间通信 |
体积和可读性是鱼和熊掌,但更关键的问题是:数据消费者是谁? 如果下游是浏览器,Parquet 再高效也白搭;如果另一端是 Go 微服务,Pickle 直接出局。
JSON:最稳的通用货币
JSON 不需要额外依赖,json 模块是标准库的一部分。它的限制同样明确——只支持基本类型,自定义对象需要你自己转换。
import json
from datetime import datetime
# 自定义对象 → JSON 的典型做法
class User:
def __init__(self, name, created_at):
self.name = name
self.created_at = created_at
def to_dict(self):
# datetime 不是 JSON 原生类型,必须转字符串
return {
"name": self.name,
"created_at": self.created_at.isoformat(),
}
@classmethod
def from_dict(cls, d):
return cls(d["name"], datetime.fromisoformat(d["created_at"]))
user = User("alice", datetime.now())
# 序列化
payload = json.dumps(user.to_dict(), ensure_ascii=False, indent=2)
print(payload)
# 反序列化
restored = User.from_dict(json.loads(payload))
print(restored.name, restored.created_at)
运行前不需要安装任何东西。ensure_ascii=False 让中文直接输出而不是 \uXXXX 转义;indent=2 方便调试,生产环境可以去掉以减小体积。
Pickle:快但危险的内部通道
Pickle 能序列化几乎所有 Python 对象,包括函数、类实例,代价是两点:只 Python 能读,反序列化不可信数据等于远程代码执行。
import pickle
# Pickle 直接序列化自定义对象,无需 to_dict
user = User("bob", datetime.now())
with open("user.pkl", "wb") as f:
pickle.dump(user, f)
with open("user.pkl", "rb") as f:
restored = pickle.load(f) # 直接拿到 User 实例
print(restored.name, restored.created_at)
关键原则:永远不要 pickle.load 来自网络或不可信来源的数据。只在自己进程间或可信存储里用它,比如把训练好的 sklearn 模型存到本地磁盘。
CSV:表格数据的最低摩擦路径
CSV 的优势不是效率,而是接收方零门槛——Excel、pandas、数据库都能直接读。
import csv
users = [
{"name": "alice", "score": 92},
{"name": "bob", "score": 85},
{"name": "carol", "score": 78},
]
# 写入
with open("scores.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["name", "score"])
writer.writeheader()
writer.writerows(users)
# 读取
with open("scores.csv", "r", encoding="utf-8") as f:
for row in csv.DictReader(f):
print(row["name"], row["score"])
CSV 只适合扁平表格。嵌套结构需要先 flatten 或改用 JSON。
Parquet:列存压缩,分析场景的性价比之王
Parquet 把数据按列存储并自动压缩,同等内容比 JSON 小 5–10 倍是常态。pandas 直接支持读写。
# 先装依赖
pip install pandas pyarrow
import pandas as pd
df = pd.DataFrame(users) # 上面的 users 列表
# 写入 Parquet,自动压缩
df.to_parquet("scores.parquet", engine="pyarrow", compression="snappy")
# 读取——只加载需要的列,省内存
df_scores = pd.read_parquet("scores.parquet", columns=["score"])
print(df_scores.describe())
当数据量从几千行涨到百万行,Parquet 的列裁剪和压缩优势会非常明显。如果下游是 Spark 或 DuckDB,Parquet 是默认选择。
Protocol Buffers:微服务通信的纪律性方案
Protobuf 强制你先写 schema(.proto 文件),再生成多语言代码。前期多一步,换来的是严格的类型约束和极小的序列化体积。
pip install protobuf
先写 schema 文件 user.proto:
syntax = "proto3";
message User {
string name = 1;
int32 score = 2;
}
用 protoc 生成 Python 代码:
protoc --python_out=. user.proto
然后在 Python 里使用:
# 生成的模块名是 user_pb2
import user_pb2
user = user_pb2.User()
user.name = "alice"
user.score = 92
# 序列化成二进制,体积远小于 JSON
data = user.SerializeToString()
print(f"字节长度: {len(data)}")
# 反序列化
restored = user_pb2.User()
restored.ParseFromString(data)
print(restored.name, restored.score)
Protobuf 的纪律性在多人协作时是优势:schema 就是契约,字段增删有明确规则(proto3 新字段不影响旧代码解析),不会出现 JSON 里"这个字段到底有没有"的模糊地带。
选型决策清单
遇到序列化需求时,按这个顺序判断:
- 下游是不是 Python?且数据来源可信? → Pickle 最省事,但别跨进程边界用。
- 需要人能直接看/编辑? → JSON(嵌套结构)或 CSV(纯表格)。
- 数据量超过百万行,下游做分析? → Parquet,配合 pandas / Spark。
- 多语言微服务高频通信? → Protobuf,用 schema 约束接口。
- 以上都不满足,只是临时存一下? → JSON,别过度设计。
一个常见陷阱是"哪个格式最先进就用哪个"。Parquet 和 Protobuf 确实高效,但如果你的数据只有 200 行、下游是 Excel 业务人员,CSV 才是正确答案。格式选择不是性能竞赛,而是匹配数据的生产者和消费者。