056、pickle 与序列化:安全性警告、协议版本、替代方案 json、msgpack
上周帮一个同事排查线上服务崩溃的问题,日志里只留下一行AttributeError: Can't get attribute 'OldModel' on <module '__main__' from ...>。一看就是 pickle 反序列化时类定义变了。这种坑我踩过不止一次,今天干脆把 pickle 的底裤扒干净,顺便聊聊什么时候该换 json 或 msgpack。
序列化到底在干什么
说白了就是把内存里的 Python 对象变成字节流,存文件、发网络、塞 Redis。反过来叫反序列化。pickle 是 Python 自带的,能序列化几乎任何对象——函数、类实例、甚至你自定义的迭代器。但代价就是它只认 Python,而且不安全。
那个让我加班的坑:协议版本
Python 2 时代默认是 protocol 0,文本格式,慢且体积大。Python 3 默认升到 protocol 3,但不同版本之间不总是兼容。我那个同事的坑是这样的:他用 Python 3.8 的 pickle 序列化了一个自定义类的实例,存到 Redis。后来代码重构,类名从OldModel改成了NewModel,反序列化时 pickle 找不到OldModel的定义,直接崩了。
别这样写:直接用默认 protocol 跨版本传输。如果你要长期存储 pickle 文件,或者在不同 Python 小版本间传递,显式指定 protocol 版本:
importpickle# 这里踩过坑:protocol 5 是 Python 3.8+ 才有的# 如果下游是 3.6,直接炸data=pickle.dumps(obj,protocol=4)# 保守点用 4,兼容 3.4+protocol 版本对照表记一下:
- 0: 文本格式,最慢,Python 2.3+
- 1: 二进制格式,Python 2.3+
- 2: Python 2.3+
- 3: Python 3.0+,默认
- 4: Python 3.4+,支持大对象
- 5: Python 3.8+,支持 out-of-band data
个人建议:跨版本存储一律用 protocol 4,除非你确定所有环境都是 3.8+。
安全性警告:别信外部数据
这是最要命的。pickle 反序列化时会执行任意代码。你从网上、用户输入、甚至内部不信任的服务收到 pickle 数据,直接pickle.loads()等于把服务器钥匙交给别人。
importpickleimportos# 恶意构造的 pickle 数据classEvil:def__reduce__(self):return(os.system,('rm -rf /',))malicious_data=pickle.dumps(Evil())# 这行执行后,你的服务器可能就没了pickle.loads(malicious_data)别这样写:永远不要对不可信数据用 pickle。内部服务之间用 pickle 也要加白名单或签名。我见过有人把 pickle 数据直接暴露在 HTTP API 里,结果被扫到直接 RCE。
替代方案:json 和 msgpack
json:安全但有限
json 只能序列化基本类型:dict、list、str、int、float、bool、None。自定义对象需要自己写 encoder/decoder。
importjsonfromdatetimeimportdatetimeclassCustomEncoder(json.JSONEncoder):defdefault(self,obj):ifisinstance(obj,datetime):returnobj.isoformat()# 这里踩过坑:忘了抛 TypeError 会导致无限递归returnsuper().default(obj)data={'time':datetime.now(),'name':'test'}json_str=json.dumps(data,cls=CustomEncoder)json 的好处是跨语言、人类可读、安全。坏处是体积大、不支持 bytes、不支持复杂对象。
msgpack:折中方案
msgpack 像 json 的二进制版本,体积小、速度快、支持 bytes。需要装第三方库msgpack。
importmsgpack data={'name':'test','score':95.5,'tags':[1,2,3]}packed=msgpack.packb(data)# 二进制,比 json 小 30% 左右unpacked=msgpack.unpackb(packed)msgpack 支持自定义类型,但需要写 hook:
importmsgpackfromdatetimeimportdatetimedefencode_datetime(obj):ifisinstance(obj,datetime):return{'__datetime__':True,'value':obj.isoformat()}returnobjdefdecode_datetime(obj):if'__datetime__'inobj:returndatetime.fromisoformat(obj['value'])returnobj packed=msgpack.packb(datetime.now(),default=encode_datetime)unpacked=msgpack.unpackb(packed,object_hook=decode_datetime)个人建议:内部服务间通信优先用 msgpack,性能好、体积小、安全。对外 API 用 json,兼容性好。只有当你确定数据只在 Python 内部流转、且需要序列化复杂对象(比如函数、类实例)时,才考虑 pickle。
实战经验总结
永远不要用 pickle 做网络传输。我见过太多人图方便把 pickle 塞进 Redis 或 RabbitMQ,结果版本升级时全崩。用 msgpack 或 json 加自定义序列化,前期多写几行代码,后期少熬几个夜。
pickle 只适合本地缓存或进程间通信。比如你训练了一个机器学习模型,用 pickle 存到本地文件,下次加载。但注意模型类定义不能变,否则反序列化失败。我一般会在 pickle 文件里加个版本号:
importpickleclassModelV1:def__init__(self,params):self.version=1self.params=params# 存的时候model=ModelV1({'lr':0.01})pickle.dump(model,open('model.pkl','wb'))# 加载的时候检查版本loaded=pickle.load(open('model.pkl','rb'))ifloaded.version!=1:raiseValueError('模型版本不匹配')如果非要用 pickle,加签名。用 hmac 对 pickle 数据做签名,反序列化前验证,防止篡改。
msgpack 的坑:它不支持
set类型,序列化时会变成 list。如果你需要 set,自己转一下。另外 msgpack 的unpackb默认返回 dict,如果你需要有序字典,用object_pairs_hook=collections.OrderedDict。性能对比:我压测过,序列化 1MB 的 dict,pickle 约 0.5ms,msgpack 约 0.3ms,json 约 0.8ms。反序列化 pickle 约 0.4ms,msgpack 约 0.2ms,json 约 0.6ms。msgpack 在体积和速度上都有优势。
最后说一句:序列化方案选型,先想清楚你的数据要活多久、要跨多少语言、要面对多少安全威胁。别让 pickle 成为你系统的后门。