Python的序列化把我坑惨了,原来json和pickle的区别这么大

简介: 本文通过小冯的缓存开发实践,对比JSON与Pickle的核心差异:JSON是跨语言、安全、人类可读的标准文本格式,但仅支持基本类型;Pickle是Python专属二进制格式,可完整保留对象类型与结构,却存在跨语言不可用、版本兼容性差及严重安全风险(可执行任意代码)。

小冯那天写了一个缓存模块,要把计算好的结果存到磁盘上,下次启动时直接读回来,避免重新计算。

计算结果是一个字典,里面有一些复杂的数据:时间戳、元组键、还有自定义对象。他先试了json:

import json

data = {
   "created_at": datetime.now(),
   "user_ids": {1, 2, 3},
   "config": {"timeout": 30},
}

with open("cache.json", "w") as f:
   json.dump(data, f)

第一行就报错了:

TypeError: Object of type datetime is not JSON serializable

他把datetime转成了字符串,又把set转成了列表,再跑一次。这次json能序列化了,但读回来的时候,时间变成了字符串,set变成了列表。他的代码后面依赖set做交集运算,依赖datetime做时间比较,读回来的数据全变了类型,下游逻辑一个个崩掉。

他换成了pickle:

import pickle

with open("cache.pkl", "wb") as f:
   pickle.dump(data, f)

with open("cache.pkl", "rb") as f:
   loaded = pickle.load(f)

一次通过。datetime还是datetime,set还是set,自定义对象还是原来的类型。小冯感叹:“pickle这么强,为什么还要用json?”

三个月后,他的服务需要从另一个语言写的服务里读取这份缓存。对方拿到.pkl文件,完全不知道这是什么格式,也找不到任何库能解析它。小冯这才意识到:pickle只认Python,出了Python的圈子,它就是一堆二进制乱码。

“原来json的‘弱’和pickle的‘强’,都是有代价的。”

这个代价,就是两者最核心的区别:**json是通用格式,pickle是Python专属格式。**

代理 IP 使用小技巧 让你的数据抓取效率翻倍 (10).png

先看一个最直观的对比

import json
import pickle

data = {"name": "Alice", "age": 30, "scores": [85, 92, 78]}

# json 序列化
json_str = json.dumps(data)
print(json_str)  # '{"name": "Alice", "age": 30, "scores": [85, 92, 78]}'

# pickle 序列化
pkl_bytes = pickle.dumps(data)
print(pkl_bytes)  # b'\x80\x04\x95...' 一堆二进制

json的输出是人类可读的文本,用{}、[]、"、,、:这些符号组织数据。你可以直接打开看,可以用文本编辑器改,可以用任何语言的JSON库解析。

pickle的输出是二进制字节流。人类看不懂,只有Python的pickle模块能读懂。它记录的不只是数据,还有Python对象的类型信息、结构信息,甚至引用关系。

用一个生活场景来理解。你要把一份文件寄给别人。

json的做法是:把文件翻译成一种国际通用语言(比如英语),写成一份标准格式的文档。对方不管用什么工具,只要懂这种语言,就能读懂。

pickle的做法是:把文件原封不动地打包成一个只有你和对方知道的密件。对方必须用和你完全相同的打包工具才能拆开。如果对方没有这个工具,或者工具版本不一样,密件就打不开。

区别一目了然:一个通用,一个专属。

json:只认基本类型

json能序列化的类型非常有限:

  • dict(键必须是字符串)
  • list
  • str
  • int
  • float
  • bool
  • None

就这些。其他类型一律报错:

import json
from datetime import datetime

json.dumps(datetime.now())  # TypeError
json.dumps({1, 2, 3})        # TypeError
json.dumps(b"bytes")         # TypeError
json.dumps((1, 2, 3))        # 可以,元组被转成列表

元组能被序列化,但读回来会变成列表,类型变了。这是一个容易忽略的细节:json不保留元组类型。

data = (1, 2, 3)
loaded = json.loads(json.dumps(data))
print(type(loaded))  # <class 'list'>,不是 tuple

json的键必须是字符串。如果你用整数或元组当键,序列化时会报错,或者键被强制转成字符串:

json.dumps({1: "a"})        # '{"1": "a"}',键变成了字符串
json.dumps({(1, 2): "a"})   # TypeError: keys must be str...

这种类型丢失在简单场景下无所谓,但在需要精确还原数据结构的场景下,就是灾难。

pickle:什么都能序列化

pickle几乎能序列化任何Python对象:

  • 基本类型(dict、list、str、int、float、bool、None)
  • tuple、set、frozenset
  • datetime、Decimal、UUID
  • 自定义类的实例
  • 函数、类(有条件)
  • 嵌套的复杂结构

import pickle
from datetime import datetime

data = {
   "created_at": datetime.now(),
   "user_ids": {1, 2, 3},
   "config": {"timeout": 30},
}

pkl = pickle.dumps(data)
loaded = pickle.loads(pkl)

print(type(loaded["created_at"]))  # <class 'datetime.datetime'>,类型保留
print(type(loaded["user_ids"]))    # <class 'set'>,类型保留

pickle不仅保存数据,还保存类型信息。读回来的时候,datetime还是datetime,set还是set,自定义对象还是原来的类。

这就是小冯最初觉得pickle“强”的原因。它不需要你手动转换类型,不需要你写自定义的编码解码逻辑,直接把对象原样存、原样取。

坑一:pickle 只能用于 Python

这是pickle最大的限制,也是小冯三个月后踩的坑。

pickle的二进制格式是Python特有的。它依赖于Python的对象模型、类定义、模块路径。其他语言没有这些概念,自然无法解析。

# 存了一个自定义对象
class User:
   def __init__(self, name):
       self.name = name

user = User("Alice")
with open("user.pkl", "wb") as f:
   pickle.dump(user, f)

这个.pkl文件里不仅存了"Alice",还存了“这是User类的实例”“User类定义在哪个模块”这些信息。换一个语言,或者换一个没有User类定义的Python环境,都读不了。

如果你需要跨语言交换数据,json是唯一的选择。json是国际标准(RFC 8259),几乎所有编程语言都有解析库。

坑二:pickle 的版本兼容性

pickle有不同的协议版本(protocol)。Python 3.8默认用协议4,Python 3.12默认用协议5。高版本Python序列化的文件,低版本Python可能读不了。

# Python 3.12 默认协议5
with open("data.pkl", "wb") as f:
   pickle.dump(data, f)

# Python 3.7 读取,可能报错
with open("data.pkl", "rb") as f:
   data = pickle.load(f)

ValueError: unsupported pickle protocol: 5

解决方案是序列化时显式指定较低的协议版本:

pickle.dump(data, f, protocol=4)  # 兼容 Python 3.4+

但即使协议版本兼容,类的定义也必须一致。如果User类的属性变了,或者类被重命名、移动了模块,旧文件也读不了。

坑三:pickle 的安全问题

这是pickle最危险的地方。

pickle.load()在反序列化时,会执行文件里记录的“重建对象”指令。恶意构造的pickle文件可以执行任意代码。

import pickle
import os

class Evil:
   def __reduce__(self):
       return (os.system, ("rm -rf /",))

# 序列化一个恶意对象
evil_data = pickle.dumps(Evil())

# 反序列化时,os.system("rm -rf /") 会被执行
pickle.loads(evil_data)

这段代码在pickle.loads()的那一刻就会执行os.system("rm -rf /")。攻击者只需要让你加载一个恶意.pkl文件,就能在你的机器上执行任意命令。

所以有一条铁律:永远不要反序列化不可信来源的pickle数据。

不可信来源包括:用户上传的文件、网络传输的数据、第三方提供的缓存文件、从数据库读出的未经校验的二进制字段。

json没有这个问题。json.loads()只解析数据,不执行代码。即使数据里有恶意内容,最多是解析失败或者得到奇怪的值,不会导致代码执行。

坑四:json 的编码问题

json.dumps()默认会把非ASCII字符转义成\uXXXX:

import json

data = {"name": "张三"}
print(json.dumps(data))  # '{"name": "\u5f20\u4e09"}'

文件里存的是一堆\u转义序列,人类可读性差。要保留原始字符,需要传ensure_ascii=False:

print(json.dumps(data, ensure_ascii=False))  # '{"name": "张三"}'

写文件时还要注意编码:

with open("data.json", "w", encoding="utf-8") as f:
   json.dump(data, f, ensure_ascii=False)

pickle没有这个问题,因为它本来就是二进制,不涉及字符编码。

坑五:json 的浮点精度

json序列化浮点数时,可能丢失精度:

import json

data = {"value": 0.1 + 0.2}
print(json.dumps(data))  # '{"value": 0.30000000000000004}'

这是浮点数本身的精度问题,不是json的错。但读回来的时候,如果下游需要精确比较,可能会出问题。

pickle能完整保留浮点数的二进制表示,不会有额外的精度损失。但浮点数本身的精度问题依然存在。

性能对比

json是文本格式,解析需要词法分析、语法分析,速度相对慢。pickle是二进制格式,解析更快。

import json
import pickle
import timeit

data = {"items": list(range(10000))}

t1 = timeit.timeit(lambda: json.dumps(data), number=1000)
t2 = timeit.timeit(lambda: pickle.dumps(data), number=1000)

print(f"json.dumps: {t1:.4f}")
print(f"pickle.dumps: {t2:.4f}")

在我的机器上,json.dumps大约0.3秒,pickle.dumps大约0.05秒。pickle快6倍左右。

体积上,pickle通常也比json小,因为二进制更紧凑。

但性能差异不应该作为选择的主要依据。大多数场景下,序列化的瓶颈在磁盘I/O或网络传输,不在序列化本身。选择json还是pickle,应该基于格式兼容性和安全性,而不是速度。

什么时候用 json,什么时候用 pickle?

判断标准很清晰:

需要跨语言交换数据:用json。这是唯一的选择。

需要人类可读、可手动编辑:用json。配置文件、API响应、日志,都应该用json。

需要长期存储、跨版本兼容:用json。json格式稳定,不会因为Python版本变化而失效。

需要存储Python特有的复杂对象(datetime、set、自定义类实例):用pickle,但要确保数据来源可信、读取环境一致。

数据来源不可信:绝对不能用pickle。用json,或者用pickle的替代品(如json加自定义编码)。

Python内部缓存、临时数据、进程间通信:可以用pickle。这些场景数据来源可控,不需要跨语言。

一个折中方案:json + 自定义编码

如果既需要json的通用性,又需要处理datetime、set这些类型,可以写自定义的编码器和解码器:

import json
from datetime import datetime

class CustomEncoder(json.JSONEncoder):
   def default(self, obj):
       if isinstance(obj, datetime):
           return {"__type__": "datetime", "value": obj.isoformat()}
       if isinstance(obj, set):
           return {"__type__": "set", "value": list(obj)}
       return super().default(obj)

def custom_decoder(dct):
   if dct.get("__type__") == "datetime":
       return datetime.fromisoformat(dct["value"])
   if dct.get("__type__") == "set":
       return set(dct["value"])
   return dct

data = {"created_at": datetime.now(), "ids": {1, 2, 3}}
json_str = json.dumps(data, cls=CustomEncoder)
loaded = json.loads(json_str, object_hook=custom_decoder)

print(type(loaded["created_at"]))  # <class 'datetime.datetime'>
print(type(loaded["ids"]))         # <class 'set'>

这样既保留了json的通用性和安全性,又能处理Python特有的类型。代价是需要手动维护编码解码逻辑,每增加一种类型就要加一个分支。

回到小冯的故事

小冯最终的方案是:缓存文件用json,但加了一层自定义编码。

class CacheEncoder(json.JSONEncoder):
   def default(self, obj):
       if isinstance(obj, datetime):
           return {"__type__": "datetime", "value": obj.isoformat()}
       if isinstance(obj, set):
           return {"__type__": "set", "value": list(obj)}
       if isinstance(obj, User):
           return {"__type__": "user", "name": obj.name}
       return super().default(obj)

def cache_decoder(dct):
   if dct.get("__type__") == "datetime":
       return datetime.fromisoformat(dct["value"])
   if dct.get("__type__") == "set":
       return set(dct["value"])
   if dct.get("__type__") == "user":
       return User(dct["name"])
   return dct

def save_cache(data, path):
   with open(path, "w", encoding="utf-8") as f:
       json.dump(data, f, cls=CacheEncoder, ensure_ascii=False)

def load_cache(path):
   with open(path, "r", encoding="utf-8") as f:
       return json.load(f, object_hook=cache_decoder)

缓存文件是文本格式,跨语言可读,跨版本兼容,没有安全风险。datetime、set、自定义对象都能正确还原。

他后来在团队文档里写了一条:“json是通用格式,只认基本类型,但跨语言、跨版本、安全。pickle是Python专属格式,什么都能存,但只认Python、版本敏感、有安全风险。需要跨语言就用json加自定义编码,只在Python内部用且数据可信才用pickle。永远不要pickle.load不可信数据。”

这个区分看起来简单,但真到写代码的时候,很多人会因为pickle方便就随手用,然后在跨语言、跨版本、或者安全审计时突然发现踩了雷。

记住那个寄文件的比喻:json是把文件翻译成国际通用语言,谁都能读;pickle是把文件打包成只有你和对方知道的密件,别人打不开,而且如果密件被掉包,拆开时可能炸。你要给谁看、数据可不可信,决定了你该用哪种方式。

目录
相关文章
|
4月前
|
API
阿里云微服务引擎 MSE 及 API 网关 2026 年 5 月产品动态
阿里云微服务引擎 MSE 及 API 网关 2026 年 5 月产品动态。
319 40
|
4月前
|
Web App开发 存储 Linux
旧手机如何组建集群,跑点云计算?
UC San Diego 与 Google 合作推进“手机集群计算”:拆解退役手机主板,组建低碳小型计算集群。首期将部署 2000 台 Pixel 手机主板,替代传统服务器,为教学科研提供低成本云资源,兼顾环保与算力复用。
463 1
旧手机如何组建集群,跑点云计算?
|
3月前
|
安全 UED
AIGC 安全治理的三道防线:输入、输出与运营闭环
AIGC 安全治理可以按三道防线落地:输入检测用于识别提示词注入、越狱和高风险意图;输出审核用于识别生成内容中的违规、侵权、误导和多模态风险;运营闭环用于沉淀日志、复核结论、样本回流和策略迭代。三者协同,才能支撑生产级 AIGC 应用稳定运行。
|
3月前
|
自然语言处理 API
字典查询-词典查询-成语词典-新华字典API接口介绍
这是一款阿里云市场提供的汉字查询API服务,支持拼音/部首/汉字/词语多维检索,返回拼音、笔画、部首、释义、出处等详细信息,适用于教育、写作、校对及识字学习场景。
440 1
|
3月前
|
人工智能 自然语言处理 算法
GEO 优化深度解析:AI 偏好 FAQ 还是长文内容?
这篇文章将从多个维度深入探讨这个问题。我们会从 AI 的认知机制出发,分析 FAQ 和长文内容各自的优势与局限;我们会引入大量的实证数据和学术研究,用事实说话
345 1
|
4月前
|
运维 应用服务中间件 网络安全
宝塔服务器报错全覆盖排查指南:新手不用盲猜,按步骤快速修复网站/面板故障
宝塔面板本身稳定性极强,绝大多数报错并非面板BUG,而是端口策略、系统资源、网站代码、权限配置四类问题。 运维排查核心思想:先外网,后内网;先系统,后服务;先日志,后重装。遇到报错不要慌乱,按照本文流程一步步定位,无需专业运维功底,也能独立
1130 6
|
4月前
|
JavaScript Shell 开发工具
给 Claude Code 做「一键安装」有多难?记一次 Electron 桌面端的踩坑之旅
Molio 是一款本地知识管理桌面应用,为降低 AI 编程门槛,实现「零配置运行 Claude Code」:无需安装 Node.js、不配 PATH、不开终端。本文详述开发中踩过的 9 大坑——从 `better-sqlite3` 崩溃、Windows PATH 玄学,到 GBK 乱码、Git Bash 依赖等,完整还原一键集成的攻坚历程。
632 0
|
5月前
|
缓存 监控 安全
别再让Docker占满你的硬盘!一篇搞定docker system所有命令
本指南详解 `docker system` 命令组,助你精准诊断与优雅清理 Docker 占用空间:`df` 查磁盘、`prune` 清资源、`info` 看配置、`events` 监事件。覆盖安全清理策略、自动化脚本与环境最佳实践,告别“磁盘爆满”焦虑。(239字)
718 2
别再让Docker占满你的硬盘!一篇搞定docker system所有命令
|
4月前
|
人工智能 缓存 弹性计算
阿里云服务器2核4G5M199元解析:独享型u1实例,性能、适用场景、购买和续费规则介绍
阿里云通用算力型u1实例(ecs.u1-c1m2.large)2核4G、5M带宽、80G ESSD Entry云盘,活动特惠价仅199元/年(官网价3498.36元),企业新老用户同享,续费同价至2027年3月31日,每人限购1台。该实例采用独享型架构,搭载Intel至强可扩展处理器,内网带宽1Gbit/s、收发包30万PPS、云盘IOPS 1万,性能稳定,适合企业官网、中小Web应用、轻量数据库及开发测试等场景。
|
4月前
|
人工智能 自然语言处理 BI
阿里云短信服务 Skill 发布:Agent 一句话搞定群发
阿里云短信服务Skill正式发布!支持资质/签名/模板查询、短信发送、记录查询及数据统计,运营人员通过自然语言即可在AI Agent中完成全流程操作,无需技术背景,告别繁琐控制台操作,提升通知与营销短信执行效率。
620 5

热门文章

最新文章