JSON 只认五种东西:字符串、数字、布尔、null,以及由它们组成的数组和对象。
datetime 不在里面,所以直接 dumps 会抛 TypeError。
pickle 存的是「这个对象在 Python 里长什么样」,所以日期读回来还是日期。
代价是只有 Python 能读。
pickle.load 在还原对象时会执行类里的构造逻辑。 一个恶意的 .pkl 文件,
可以让你在打开它的瞬间跑起别人的代码。
上一篇讲怎么组织数据,这一篇讲怎么把它存成文件。 教材第 8 章后两节是【网络应用程序消息交换格式】和【保存 Python 原生数据】。 前者的主角是 JSON 和 XML,后者是 pickle。三种格式,三套适用场合。 本篇有一处反直觉的数字:pickle 存这一小份数据反而比 JSON 大。 演示里如实摆出来了, 也解释了为什么「省空间」从来不是选 pickle 的理由。
一、同一份数据,三种写法
演示用的数据是一条订单:单号、日期、金额、备注。注意日期是 datetime 对象,不是字符串。
这个细节是整篇的关键。如果日期本来就是字符串,三种格式都不会有问题;
正因为它是真对象,才分出了高下。
四个字段:单号是字符串、日期是 datetime(2026, 11, 2, 9, 30)、金额是 328.5、备注是「含运费」。
真实业务里日期几乎都是对象,因为它要参与计算(差几天、是不是本月)。 存的时候变成字符串,取出来要再解析回对象——这一步的麻烦程度,决定了你选哪种格式。
二、JSON 碰上 datetime 会怎样
json.dumps(数据) 直接抛 TypeError: Object of type datetime is not JSON serializable。
原因很干脆:JSON 标准里只有五种东西——字符串、数字、布尔、null,以及由它们组成的数组和对象。
datetime 不在里面。
最省事的兜底是 json.dumps(d, default=str)。
它碰到不认识的对象就调 str(),于是日期变成 "2026-11-02 09:30:00"。
代价是读回来之后它就是个普通字符串,不再是时间对象。 想再当时间用,
得自己解析(datetime.strptime,第 4.2 篇讲过)。
更讲究的做法是传一个函数进去:碰到 datetime 就转成 ISO 格式字符串,读的时候再转回来。
这样格式统一,别的语言也认。
为什么 JSON 这么严格?因为它要「所有语言都能读」。
要是允许各语言塞自己的对象进去,Python 存的日期 Java 就读不懂了。限制是为了通用。
三、XML 长什么样
它长这样:外层一个 order 标签,里面四个子标签 no、date、amount、note,
每个子标签里夹着值。整串排下来一行放不下,所以演示里是打印出来的。
标签套标签,比 JSON 啰嗦一倍。 同一个 PO-001,JSON 里写 "单号": "PO-001",
XML 里要写 <no>PO-001</no>。
但 XML 有两样 JSON 没有的东西:属性和命名空间。 属性写 <no lang="zh">PO-001</no>,
命名空间用来区分「同一个名字在不同系统里的不同意思」。
所以老的系统集成、配置文件、Office 文档内部(.docx 其实就是一堆 XML 打包成 zip),
到今天还在用 XML。
解析用标准库的 xml.etree.ElementTree,几行就能读出来。
注意解析出来的东西永远是字符串,amount 要算数得自己 float()。
这一点跟 CSV 一样:格式本身不带类型信息,类型全靠你解读。
四、pickle:Python 自己的存法
| 格式 | 字节数 | 日期还是时间对象吗 | 别的语言能读吗 |
|---|---|---|---|
| JSON | 93 | 不是,变成字符串了 | 能 |
| XML | 106 | 不是 | 能 |
| pickle | 123 | 是 | 不能 |
pickle 存的是「这个对象在 Python 里长什么样」,所以日期读回来还是 datetime,
你自己定义的类实例也能原样存。
代价是只有 Python 能读。别的语言打不开,甚至不同 Python 版本之间也可能读不了。
注意这次 pickle 反而比 JSON 大。 93 比 123,小了 30 字节。
为什么?数据这么小的时候,pickle 要额外记类型标记——「这里是个 datetime」这句话本身就要占字节。 数据量上去、结构复杂以后,pickle 才开始占便宜。 但省不省空间从来不是选它的理由,「类型不丢」才是。 你有一堆自定义类的对象、读回来还要接着用, 那就 pickle;只是存个数字表格,用不着它。
五、pickle 的坑:别读来路不明的文件
pickle.load 在还原对象时会执行类里的构造逻辑。 也就是说,一个精心构造的 .pkl 文件,
可以让你在打开它的瞬间跑起别人的代码。
这不是 bug,是设计。pickle 的整个机制就是「按记录重建对象」,重建就得执行代码。
所以规矩很硬:
pickle 只用来存自己程序生成的东西。永远不要 load 从网上下载的、或者别人发给你的 .pkl。
这一条没有例外。哪怕对方是你同事,他电脑上也可能已经中招了。 要跨程序、跨语言交换数据,用 JSON。要对人可读、能手改,用 JSON 或 CSV。 要原样保存 Python 的一堆对象、且只在自己机器上用,才轮到 pickle。
六、三种格式怎么选
| 场景 | 选什么 | 原因 |
|---|---|---|
| 给前端接口返回数据 | JSON | 所有语言都认,浏览器直接能用 |
| 给 Excel 或老系统 | CSV | 双击就开,改起来最省事 |
| 对接企业已有系统 | XML | 对方只认这个,你没得选 |
| 存自己程序的中间结果 | pickle | 类型不丢,读写都快 |
| 人要看、还要能手改的配置 | JSON 或 TOML | 文本格式,改错也看得出来 |
判断标准只有一条:这份数据以后谁要读。 只有你自己读,挑最省事的;别人要读,挑他能打开的。这一条能解决九成的选择困难。 还有一条隐含的:这份数据要不要活很久? 活很久的用文本格式(JSON、CSV、XML), 因为二十年后的程序还能读它。pickle 不行,Python 版本一换就可能读不了。
本书后面几章都在跟数据库打交道。 到那时候你会发现,上面这些格式处理的是「一小份数据」,数据库处理的是「一大堆还要能查」。 两者的分界线是「要不要按条件筛选」。
七、动手 Demo:三种存法:JSON、XML、pickle
同一份带 datetime 的数据,用三种格式各存一遍。 先看 JSON 直接存会报什么错,再看 pickle 为什么能保住类型。 最后比字节数——注意 pickle 这次反而更大。
import json, pickle, os, tempfile
import xml.etree.ElementTree as ET
from datetime import datetime
d = tempfile.mkdtemp()
data = {'单号': 'PO-001', '日期': datetime(2026, 11, 2, 9, 30),
'金额': 328.5, '备注': '含运费'}
print('① 同一份数据,三种写法')
print(' 数据里的日期是 datetime 对象,不是字符串')
print(' ', data)
print('② JSON 碰上 datetime 会怎样')
try:
json.dumps(data, ensure_ascii=False)
except TypeError as e:
print(' json.dumps → TypeError:', e)
js = json.dumps(data, ensure_ascii=False, default=str)
print(' 加 default=str 之后:')
print(' ', js)
print(' 读回来日期是:', type(json.loads(js)['日期']).__name__)
print('③ XML 长什么样')
root = ET.Element('order')
for k, v in (('no', 'PO-001'), ('date', '2026-11-02T09:30:00'),
('amount', '328.5'), ('note', '含运费')):
ET.SubElement(root, k).text = v
xml_s = ET.tostring(root, encoding='unicode')
print(' ', xml_s)
print(' 解析回来:')
for ch in ET.fromstring(xml_s):
print(' %-8s %s' % (ch.tag, ch.text))
print(' XML 用标签套标签,比 JSON 啰嗦一倍,但能带属性和命名空间。')
print(' 解析出来的东西永远是字符串。要数字得自己 float()。')
print('④ pickle:Python 自己的存法')
pkl = pickle.dumps(data)
print('| 格式 | 字节数 | 日期还是时间对象吗 | 别的语言能读吗 |')
print('|---|---:|---:|---|')
print('| JSON | %d | 不是,变成字符串了 | 能 |' % len(js.encode('utf-8')))
print('| XML | %d | 不是 | 能 |' % len(xml_s.encode('utf-8')))
print('| pickle | %d | 是 | 不能 |' % len(pkl))
back = pickle.loads(pkl)
print(' pickle 读回来:type(日期) =', type(back['日期']).__name__)
print(' 值 =', back['日期'])
print(' 注意这次 pickle 反而比 JSON 大:数据这么小的时候,它要额外记类型标记,亏了。')
print('⑤ pickle 的坑:别读来路不明的文件')
print(' pickle.load 在还原对象时会执行类里的构造逻辑')
print(' 也就是说:一个恶意的 .pkl 文件,可以让你在打开它的瞬间跑起别人的代码')
print(' 所以规矩很硬:pickle 只用来存自己程序生成的东西,永远不要 load 从网上下载的、')
print(' 或者别人发给你的 .pkl。')
print('⑥ 三种格式怎么选')
print('| 场景 | 选什么 | 原因 |')
print('|---|---|---|')
for r in (('给前端接口返回数据', 'JSON', '所有语言都认,浏览器直接能用'),
('给 Excel 或老系统', 'CSV', '双击就开,改起来最省事'),
('对接企业已有系统', 'XML', '对方只认这个,你没得选'),
('存自己程序的中间结果', 'pickle', '类型不丢,读写都快'),
('人要看、还要能手改的配置', 'JSON 或 TOML', '文本格式,改错也能看出来')):
print('| %s | %s | %s |' % r)
print(' 判断标准只有一条:这份数据以后谁要读。')
实跑输出:
--------------------------------------------------------------------
三种存法:JSON、XML、pickle
--------------------------------------------------------------------
① 同一份数据,三种写法
--------------------------------------------------------------------
数据里的日期是 datetime 对象,不是字符串
{'单号': 'PO-001', '日期': datetime.datetime(2026, 11, 2, 9, 30), '金额': 328.5, '备注': '含运费'}
② JSON 碰上 datetime 会怎样
--------------------------------------------------------------------
json.dumps → TypeError: Object of type datetime is not JSON serializable
加 default=str 之后:
{"单号": "PO-001", "日期": "2026-11-02 09:30:00", "金额": 328.5, "备注": "含运费"}
JSON 只认五种东西:字符串、数字、布尔、null、以及由它们组成的数组和对象。
datetime 不在里面,所以得自己转。
default=str 是最省事的兜底,代价是读回来之后它就是个普通字符串,不再是时间对象——想再当时间用,
得自己解析。
③ XML 长什么样
--------------------------------------------------------------------
<order><no>PO-001</no><date>2026-11-02T09:30:00</date><amount>328.5</amount><note>含运费</note></order>
解析回来:
no PO-001
date 2026-11-02T09:30:00
amount 328.5
note 含运费
XML 用标签套标签,比 JSON 啰嗦一倍,但能带属性和命名空间,老的系统集成、配置文件、
Office 文档内部都用它。
解析出来的东西永远是字符串。要数字得自己 float()。
④ pickle:Python 自己的存法
--------------------------------------------------------------------
| 格式 | 字节数 | 日期还是时间对象吗 | 别的语言能读吗 |
|---|---:|---:|---|
| JSON | 93 | 不是,变成字符串了 | 能 |
| XML | 106 | 不是 | 能 |
| pickle | 123 | 是 | 不能 |
pickle 读回来:type(日期) = datetime
值 = 2026-11-02 09:30:00
pickle 存的是「这个对象在 Python 里长什么样」,所以日期读回来还是日期,
你自己定义的类实例也能原样存。代价是只有 Python 能读,别的语言打不开。
注意这次 pickle 反而比 JSON 大:数据这么小的时候,它要额外记类型标记,亏了。
数据量上去、结构复杂以后,pickle 才开始占便宜——但省不省空间从来不是选它的理由,
「类型不丢」才是。
⑤ pickle 的坑:别读来路不明的文件
--------------------------------------------------------------------
pickle.load 在还原对象时会执行类里的构造逻辑
也就是说:一个恶意的 .pkl 文件,可以让你在打开它的瞬间跑起别人的代码
所以规矩很硬:pickle 只用来存自己程序生成的东西,永远不要 load 从网上下载的、
或者别人发给你的 .pkl。
要跨程序、跨语言交换数据,用 JSON。要对人可读、能手改,用 JSON 或 CSV。
要原样保存 Python 的一堆对象、且只在自己机器上用,才轮到 pickle。
⑥ 三种格式怎么选
--------------------------------------------------------------------
| 场景 | 选什么 | 原因 |
|---|---|---|
| 给前端接口返回数据 | JSON | 所有语言都认,浏览器直接能用 |
| 给 Excel 或老系统 | CSV | 双击就开,改起来最省事 |
| 对接企业已有系统 | XML | 对方只认这个,你没得选 |
| 存自己程序的中间结果 | pickle | 类型不丢,读写都快 |
| 人要看、还要能手改的配置 | JSON 或 TOML | 文本格式,改错也能看出来 |
判断标准只有一条:这份数据以后谁要读。只有你自己读,挑最省事的;别人要读,挑他能打开的。
第一段那个 datetime 对象是整篇的关键。 日期要是本来就是字符串,三种格式都不会出问题,也就分不出高下了。 第二段那个 TypeError 值得读原文:not JSON serializable,意思是「不能序列化成 JSON」。 default=str 是最省事的兜底,代价是读回来就只是字符串了。 第三段 XML 那串要横着看:一对标签包一个值。比 JSON 啰嗦,但它能带属性,老系统就认这个。 第四段那个 123 比 93 要看清:pickle 存这一小份反而更大。 因为它要额外记「这里是个 datetime」,这句说明本身就要占字节。 省不省空间从来不是选 pickle 的理由,类型不丢才是。 第五段那条规矩没有例外。 pickle 的机制就是「按记录重建对象」,重建就得执行代码,所以恶意文件能在你打开它的瞬间跑起来。
30 秒实验
- 把演示第一段的
datetime(2026, 11, 2, 9, 30)换成字符串'2026-11-02',重跑, 看 JSON 还会不会报错,pickle 的字节数变多少。 - 把演示第二段的
default=str去掉,改成自己写一个转换函数, 让日期输出成 ISO 格式(isoformat())。 - 把演示第四段的数据里加一个列表(比如两个商品明细),再看三种格式的字节数差多少。
常见误区
- 「JSON 能存任何 Python 对象。」只认五种东西:字符串、数字、布尔、null,以及它们的组合。 日期、集合、自定义类都不行,得自己转。
- 「pickle 比 JSON 小,所以存数据该用 pickle。」演示里它反而更大。 省不省空间从来不是选它的理由,「类型不丢」才是。 而且它只有 Python 能读。
- 「pickle 读个文件而已,能有什么风险。
」
pickle.load会执行代码。 来路不明的.pkl等于一个可执行文件,绝不能打开。
自测三问(点开对答案)
1. `json.dumps` 碰到 `datetime` 为什么报错?怎么办? —— 因为 JSON 标准只认五种东西:字符串、数字、布尔、null 和它们的组合,没有日期类型。 最简单的办法是 `default=str` 把它转成字符串; 讲究一点就传个函数转成 ISO 格式,读的时候再 `strptime` 解析回来。 2. pickle 为什么能保住 `datetime` 类型,JSON 不行? —— 因为 pickle 存的是「这个对象在 Python 里长什么样」,包括它的类型,读的时候按记录重建。 JSON 是给所有语言用的通用格式,只能存那五种基本类型,类型信息就丢了。 代价是 pickle 只有 Python 能读。 3. 为什么绝对不能 load 别人发来的 `.pkl` 文件? —— 因为 `pickle.load` 在还原对象时会执行类里的构造逻辑。 一个精心构造的 `.pkl` 可以让你的程序在打开它的瞬间跑起别人的代码。 这不是漏洞,是 pickle 的机制决定的——它必须执行代码才能重建对象。第 8 章结束。下一篇进第 9 章:用 Flask 起一个真能访问的网站。
- ☐ (本篇完,下一篇 9.1 HTTP 与第一个 Flask 网站)