查看: 176|回复: 3

Python json模块序列化datetime报错与流式解析

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
一、先弄清JSON与Python的类型映射

JSON是跨语言的数据交换格式,Python标准库的json模块负责在两者之间做双向转换。转换规则是固定的,记牢这张对应表可以避免大部分“类型对不上”的低级错误:
  1. # JSON类型 -> Python类型
  2. # object           -> dict
  3. # array            -> list
  4. # string           -> str
  5. # number(int)      -> int
  6. # number(real)     -> float
  7. # true / false     -> True / False
  8. # null             -> None
复制代码

反向序列化时,dict、list、str、int、float、bool、None都能直接写出;而datetime、date、自定义类实例、set等类型不在JSON规范内,这是后文TypeError的根源。

二、四个核心函数的分工

json模块对外最常用的只有四个函数,按“操作对象”划分:dumps()/loads() 处理字符串,dump()/load() 处理文件句柄。两者参数基本一致,只是后者多了一个文件对象参数。

2.1 json.dumps():Python对象转JSON字符串
  1. import json
  2. data = {
  3.     "name": "张三",
  4.     "age": 25,
  5.     "languages": ["Python", "JavaScript"],
  6.     "active": True,
  7.     "score": None,
  8.     "address": {"city": "北京", "district": "海淀"},
  9. }
  10. # 默认输出会把中文转成\uXXXX,加 ensure_ascii=False 才能保留中文
  11. json_str = json.dumps(data, ensure_ascii=False)
  12. print(json_str)
  13. # 紧凑格式:separators 指定元素间与键值间的分隔符
  14. compact = json.dumps(data, ensure_ascii=False, separators=(",", ":"))
  15. print(compact)
  16. # 美化输出:indent 指定缩进空格数
  17. pretty = json.dumps(data, ensure_ascii=False, indent=2)
  18. print(pretty)
  19. # 键排序:sort_keys=True,便于做diff比对或生成稳定签名
  20. sorted_keys = json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True)
复制代码

几个参数的适用场景值得单独说:ensure_ascii=False 用于任何要给人看或要落盘的中文数据;separators 用于网络传输,去掉多余空白能减小体积;indent 用于配置文件,方便人工维护;sort_keys 用于需要输出稳定文本的场合,比如接口签名或版本控制下的配置比对。

2.2 json.loads():JSON字符串转Python对象
  1. import json
  2. json_str = '''
  3. {
  4.     "name": "李四",
  5.     "age": 30,
  6.     "skills": ["Python", "Django", "SQL"],
  7.     "is_admin": true,
  8.     "profile": null
  9. }
  10. '''
  11. data = json.loads(json_str)
  12. print(f"姓名: {data['name']}")
  13. print(f"年龄: {data['age']}")
  14. print(f"技能: {', '.join(data['skills'])}")
  15. print(f"管理员: {data['is_admin']}")   # JSON的true -> Python的True
  16. print(f"头像: {data['profile']}")      # JSON的null -> Python的None
复制代码

注意JSON里的true/null在前端和Python里写法不同,反序列化后自动变成True/None,不要再按字符串"true"去判断。

2.3 json.dump()与json.load():直接读写文件
  1. import json
  2. config = {
  3.     "app_name": "MyApp",
  4.     "version": "1.0.0",
  5.     "database": {
  6.         "host": "localhost",
  7.         "port": 5432,
  8.     },
  9.     "features": ["auth", "logging", "api"],
  10. }
  11. # 写入文件
  12. with open("config.json", "w", encoding="utf-8") as f:
  13.     json.dump(config, f, ensure_ascii=False, indent=2)
  14. # 从文件读取
  15. with open("config.json", "r", encoding="utf-8") as f:
  16.     loaded = json.load(f)
  17. print(loaded["app_name"])   # MyApp
  18. print(loaded == config)     # True
复制代码

文件读写务必显式指定encoding="utf-8",否则在Windows默认编码下写中文可能抛UnicodeEncodeError,或读回时出现乱码。

三、非标准类型:datetime为什么报TypeError

json模块只认识上表中的七种类型。直接序列化datetime会抛异常:
  1. import json
  2. from datetime import datetime
  3. # json.dumps({"time": datetime.now()})   # TypeError: Object of type datetime is not JSON serializable
复制代码

报错信息里已点明原因:datetime不是可序列化类型。json模块在遇到未知类型时会回调default()方法,默认实现直接抛出上面的TypeError。因此解决思路就是接管这个回调,有下面三种写法。

方案一:自定义JSONEncoder子类
  1. import json
  2. from datetime import datetime, date
  3. class DateTimeEncoder(json.JSONEncoder):
  4.     def default(self, obj):
  5.         if isinstance(obj, (datetime, date)):
  6.             return obj.isoformat()
  7.         return super().default(obj)
  8. data = {
  9.     "user": "张三",
  10.     "created_at": datetime.now(),
  11.     "birthday": date(1995, 8, 20),
  12. }
  13. json_str = json.dumps(data, cls=DateTimeEncoder, ensure_ascii=False)
  14. print(json_str)
复制代码

这种方式适合项目中多处需要序列化时间字段的场景,编码器可以复用,也便于统一时间格式。

方案二:传default函数
  1. import json
  2. from datetime import datetime, date
  3. def custom_serializer(obj):
  4.     if isinstance(obj, datetime):
  5.         return obj.isoformat()
  6.     if isinstance(obj, date):
  7.         return obj.isoformat()
  8.     raise TypeError(f"无法序列化类型: {type(obj)}")
  9. json_str = json.dumps(data, default=custom_serializer, ensure_ascii=False)
复制代码

适合一次性、临时性的序列化需求。注意未知类型要主动raise TypeError,否则会被静默吞掉,排查时反而更难定位。

方案三:序列化前先转成基础类型
  1. data_serializable = {
  2.     "user": "张三",
  3.     "created_at": datetime.now().isoformat(),
  4.     "birthday": date(1995, 8, 20).isoformat(),
  5. }
  6. json_str = json.dumps(data_serializable, ensure_ascii=False)
复制代码

这是最容易理解、最不依赖json模块特性的做法,缺点是数据一旦嵌套较深,手工转换的代码会变得啰嗦且容易漏字段。

四、自定义解码:object_hook与object_pairs_hook

反序列化时同样可以把特定字段还原成Python对象,入口是object_hook——它会在每个dict构造完成后被调用。
  1. import json
  2. from datetime import datetime
  3. json_str = '''
  4. {
  5.     "user": "张三",
  6.     "created_at": "2024-06-15T14:30:00",
  7.     "login_count": "100"
  8. }
  9. '''
  10. def custom_decoder(dct):
  11.     """自动将isoformat时间字符串转为datetime"""
  12.     result = {}
  13.     for key, value in dct.items():
  14.         if isinstance(value, str):
  15.             try:
  16.                 result[key] = datetime.fromisoformat(value)
  17.                 continue
  18.             except (ValueError, TypeError):
  19.                 pass
  20.         result[key] = value
  21.     return result
  22. data = json.loads(json_str, object_hook=custom_decoder)
  23. print(f"类型: {type(data['created_at'])}")   # <class 'datetime.datetime'>
复制代码

这段实现有一个必须注意的副作用:它对所有字符串都尝试解析时间,普通文本只要能被fromisoformat识别就会被误转换。生产中更稳妥的做法是按字段名白名单判断,例如只处理key为created_at、updated_at这类字段。

如果只是想让字典保持JSON中的原始键顺序,可以用object_pairs_hook:
  1. data_ordered = json.loads(json_str, object_pairs_hook=dict)
复制代码

五、类对象与嵌套数据的序列化

json模块不会自动识别自定义类,需要在类上自己暴露to_dict()/from_dict(),把对象和字典之间的转换显式化。
  1. import json
  2. class User:
  3.     def __init__(self, name, email, roles=None):
  4.         self.name = name
  5.         self.email = email
  6.         self.roles = roles or []
  7.     def to_dict(self):
  8.         return {"name": self.name, "email": self.email, "roles": self.roles}
  9.     @classmethod
  10.     def from_dict(cls, data):
  11.         return cls(data["name"], data["email"], data.get("roles", []))
  12.     def to_json(self):
  13.         return json.dumps(self.to_dict(), ensure_ascii=False)
  14. # 序列化
  15. user = User("张三", "zs@test.com", ["admin", "editor"])
  16. json_str = user.to_json()
  17. print(json_str)
  18. # 反序列化
  19. data = json.loads(json_str)
  20. user2 = User.from_dict(data)
  21. print(user2.name, user2.email, user2.roles)
复制代码

这种写法的好处是:序列化边界清晰,嵌套对象只需在to_dict()里递归展开;from_dict()用data.get("roles", [])给可选字段兜底,避免旧数据缺字段时直接KeyError。

六、大文件流式解析:raw_decode逐条读取

json.load()会把整个文件一次性载入内存,几百MB的JSON数组很容易把内存吃满。这种情况下可以用JSONDecoder.raw_decode()做增量解析,边读边产出对象。
  1. import json
  2. def stream_parse_json_array(filepath):
  3.     """流式解析JSON数组——逐条产出对象"""
  4.     with open(filepath, "r", encoding="utf-8") as f:
  5.         # 跳过开头的 [
  6.         char = f.read(1)
  7.         while char and char != '[':
  8.             char = f.read(1)
  9.         decoder = json.JSONDecoder()
  10.         buffer = ""
  11.         for line in f:
  12.             line = line.strip()
  13.             if line in (']', '],'):
  14.                 break   # 数组结束
  15.             if line.endswith(','):
  16.                 line = line[:-1]
  17.             buffer += line
  18.             try:
  19.                 obj, _ = decoder.raw_decode(buffer)
  20.                 yield obj
  21.                 buffer = ""
  22.             except json.JSONDecodeError:
  23.                 continue   # 数据不完整,等待下一行
复制代码

构造测试数据验证一下:
  1. with open("large.json", "w") as f:
  2.     f.write("[\n")
  3.     for i in range(5):
  4.         f.write(f'  {{"id": {i}, "name": "item{i}"}}')
  5.         f.write(",\n" if i < 4 else "\n")
  6.     f.write("]")
  7. for item in stream_parse_json_array("large.json"):
  8.     print(item)
复制代码

这套实现的前提是“每行一个JSON对象”,即常说的JSON Lines排版风格。如果源文件是整行压缩成一个巨型数组的单行JSON,逐行读的逻辑就不成立,需要改造成按字符块累加buffer再尝试raw_decode。另外raw_decode返回的是(对象, 结束位置)二元组,本例丢弃了结束位置,只适用于一行恰好一个对象的场景。

七、结构校验:不引第三方库的轻量方案

很多场景不需要完整的JSON Schema,只要确认必填字段在不在、类型对不对。写个几十行的校验函数就够了。
  1. import json
  2. def validate_json_structure(data, required_fields, field_types=None):
  3.     """验证JSON数据的结构"""
  4.     errors = []
  5.     for field in required_fields:
  6.         if field not in data:
  7.             errors.append(f"缺少必填字段: {field}")
  8.     if field_types:
  9.         for field, expected_type in field_types.items():
  10.             if field in data and not isinstance(data[field], expected_type):
  11.                 errors.append(
  12.                     f"字段{field}类型错误: "
  13.                     f"期望{expected_type.__name__}, "
  14.                     f"实际{type(data[field]).__name__}"
  15.                 )
  16.     return len(errors) == 0, errors
  17. user_schema = {
  18.     "required_fields": ["name", "email"],
  19.     "field_types": {"name": str, "email": str, "age": int},
  20. }
  21. data = {"name": "张三", "email": "zs@test.com", "age": "二十五"}
  22. valid, errors = validate_json_structure(
  23.     data,
  24.     user_schema["required_fields"],
  25.     user_schema["field_types"]
  26. )
  27. print(f"验证: {'通过' if valid else '失败'}")
  28. for e in errors:
  29.     print(f" - {e}")
复制代码

这个函数返回布尔值和错误列表,便于在接口层统一汇总报错。要留意isinstance对bool的判断:Python中bool是int的子类,若字段期望int而实际传入True,校验会通过,需要额外排除。

八、实践要点汇总

1. dumps()/loads() 面向字符串,dump()/load() 面向文件对象,参数含义一致。
2. ensure_ascii=False 才能输出可读中文;indent=2 用于美化;separators=(",", ":") 用于压缩;sort_keys=True 用于输出稳定文本。
3. datetime/date 等类型无法直接序列化,报错信息为“not JSON serializable”,用JSONEncoder子类、default回调或预先isoformat()转换三种方式解决。
4. 反序列化还原时间字段用object_hook,但要按字段名限定,避免把普通字符串误转成datetime。
5. 自定义类通过to_dict()/from_dict()显式转换,可选字段用dict.get()兜底。
6. 超大JSON数组用JSONDecoder.raw_decode()流式处理,前提是文件为逐行对象格式。
7. 字段与类型校验可以自己实现,不必为简单场景引入Schema依赖。

JSON在API交互、配置落盘、数据传输中几乎无处不在,json模块本身足够用,真正需要额外投入精力的地方在于非标准类型的边界处理和内存占用控制。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Python json模块序列化datetime报错与流式解析

这篇整理得很清楚,类型映射表和四个函数的分工一目了然,ensure_ascii、separators、indent、sort_keys 的适用场景总结得很实用。文件读写显式指定 utf-8 这点也很关键。正文末尾好像截断了,期待后续把 datetime 序列化和流式解析补上。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Python json模块序列化datetime报错与流式解析

楼主整理得很清晰,类型映射那张表确实值得背下来,新手最容易在 datetime、set 这些非 JSON 类型上踩坑。四个函数按操作对象分成字符串和文件句柄两类,这个划分方式比逐个罗列参数好记得多。 几个参数的适用场景总结得挺实用:ensure_ascii=False 落盘和给人看必备,separators 去掉空白减小传输体积,indent 配配置文件,sort_keys 做签名或 diff 比对时确实省心,不然每次键顺序变一下文本就全变了。 另外文件读写显式指定 encoding="utf-8" 这点真的容易被忽略,尤其 Windows 上默认编码不是 utf-8,不加的话中文写进去再读出来就容易出问题。建议写入时再顺手把 newline 之类的问题也留意下。 不过看标题是讲 datetime 序列化报错和流式解析的,正文好像到文件读写这里就断掉了,后面 TypeError 的根源和流式解析的部分还没发出来吧?挺期待楼主继续补上 default 参数、JSONEncoder 子类以及 ijson 那块的思路,先收藏了。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Python json模块序列化datetime报错与流式解析

楼主整理得很清楚,类型映射表和四个函数的参数场景说明特别实用,尤其是 ensure_ascii、separators、indent、sort_keys 这几个的适用场合,平时确实容易混着用。 不过标题里说的 datetime 序列化报错和流式解析,正文好像还没展开到,最后那句“务必显式指定 encoding”也被截断了,估计是发帖时被截掉了。关于 datetime,我平时常用两种思路:一是给 dumps 传 default 参数,把 datetime/date 转成 isoformat 字符串;二是自定义一个 JSONEncoder 子类重写 default 方法,复用性更好。反序列化那边则用 object_hook 或者手动把字符串再转回 datetime,看需求要不要保留时区。 流式解析如果是指超大 JSON 文件,json 模块本身不支持增量解析,一般会考虑 ijson 之类的库,按事件逐个读取,内存占用会小很多。期待楼主把后半部分补完,特别是流式解析这块想看看具体怎么落地。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-11-9 04:51 , Processed in 0.022868 second(s), 29 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部