Featured image of post Python 中将字符串解析为对象的四种方法

Python 中将字符串解析为对象的四种方法

json.loads、ujson.loads、ast.eval、ast.literal_eval 的区别

Python 中有四种将字符串解析为对象的方法,除了常见的反序列化工具 json.loadsujson.loads,还有 evalast.literal_eval 两个函数。下表概括了四者的差异:

json.loads ujson.loads eval ast.literal_eval
来源 标准库 json 三方库 ujson 标准库 ast 标准库 ast
安全性 安全 安全 不安全 安全
输入 严格 JSON 严格 JSON 任意 Python 语句 Python 字面量
支持类型 JSON 6种类型 JSON 6种类型 全部 Python 类型 str/int/list/dict/tuple/set
单引号字符串 不支持 不支持 支持 支持
性能 最快 较慢 较慢

json.loads — 标准 JSON 解析

Python 内置 json 模块用纯 Python 实现了一个递归下降 JSON 解析器。CPython 3.x 中核心部分(_json 模块)实际是 C 扩展加速,但逻辑上严格遵循 RFC 8259

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import json

# 基本用法
data = json.loads('{"key": "value", "num": 42, "flag": true}')

# 自定义对象钩子
def parse_decimal(d):
    return {k: float(v) if isinstance(v, str) else v for k, v in d.items()}

data = json.loads('{"price": "3.14"}', object_hook=parse_decimal)

# 严格的格式要求(常见坑)
json.loads("{'key': 'val'}")   # ValueError:单引号不合法
json.loads('{"key": None}')    # ValueError:None 是 Python 不是 JSON(应该是 null)
json.loads('{"key": undefined}')  # undefined 不是有效 JSON

JSON 与 Python 类型映射

JSON Python
object dict
array list
string str
number (int) int
number (float) float
true/false True/False
null None

注意点:JSON 的 key 必须是双引号字符串;不支持注释;NaN/Infinity 默认不支持(但可通过 parse_constant 兼容)。


ujson.loads — 超快速 JSON 解析

ujson(UltraJSON)是用 C 写的第三方 JSON 库,底层直接操作内存、避免了 CPython 对象创建的开销,对大型 JSON 文档比标准库快 2~5 倍。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import ujson

data = ujson.loads('{"key": "value"}')

# API 和 json.loads 基本兼容
ujson.loads('{"ts": 1700000000}')

# 但有细微差异:
ujson.loads('{"val": 1.000000000000001}')  # 浮点精度可能与 json 略有差异
ujson.loads('NaN')    # 部分版本 ujson 会把 NaN 当合法值处理,而 json 会抛异常

注意,ujson 并非 100% 兼容 json 模块;object_hook 等钩子支持不完整;对某些边缘 Unicode 转义的处理历史上有 bug;生产环境建议锁定版本。

实际上 json.loads 在多数场景下性能已经足够,无需额外引入 ujson 增加系统复杂度。


eval() — 绝对不要用于外部输入

Python 内置函数,将字符串送入 Python 解释器的代码编译+执行流水线,和直接执行 Python 代码完全等价。

1
2
3
4
5
6
7
8
# 能用
eval('{"key": "value"}')   # 返回 dict
eval('[1, 2, 3]')          # 返回 list

# 但不安全
eval("__import__('os').system('rm -rf /')")
eval("open('/etc/passwd').read()")
eval("__import__('subprocess').check_output(['whoami'])")

eval() 对不可信输入的任何使用场景都有替代方案,永远不要用于解析外部数据。


ast.literal_eval — 安全的 Python 字面量解析

ast.literal_eval 先用 Python 编译器将字符串解析为 AST(抽象语法树),然后只允许 AST 中出现字面量节点ConstantListDictTupleSet),遇到任何函数调用、名称引用等节点都会抛出 ValueError,不会执行任何代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import ast

# 比 json.loads 宽松的地方:支持 Python 语法
ast.literal_eval("{'key': 'value'}")          # ✓ 单引号 OK
ast.literal_eval("[1, 2, 3]")                 # ✓ list
ast.literal_eval("(1, 2, 3)")                 # ✓ tuple(json 不支持)
ast.literal_eval("{1, 2, 3}")                 # ✓ set(json 不支持)
ast.literal_eval("{'a': None, 'b': True}")    # ✓ None/True/False(Python 风格)
ast.literal_eval("3.14")                      # ✓ 数字字面量
ast.literal_eval("b'bytes'")                  # ✓ Python 3 bytes 字面量

# 不支持的
ast.literal_eval("[x for x in range(10)]")   # ValueError:有表达式
ast.literal_eval("1 + 2")                    # ValueError(3.2以后)
ast.literal_eval("os.getcwd()")              # ValueError:有调用

经典的使用场景是解析 Python repr() 的输出,或来自可信内部系统(但非完全受信任)的配置字符串,解析 set/tuple 等 JSON 无法表达的类型。

注意:

  • 对超大嵌套结构(几千层递归)有栈溢出风险(CPython 有递归深度限制),建议在解析前做大小/深度预检。
  • 解析速度比 json.loads 慢 5~10x,不适合高频路径。
  • Python 3.2 之前 1+2 会返回 3(有一个算术折叠的 bug),之后修复了。
  • 对于大字符串,它实际上会构建完整 AST 再遍历,内存开销比 json 高。

常见混淆场景

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import json, ast

# 场景1:Python repr 的 dict(单引号)
s = "{'a': 1, 'b': True}"
json.loads(s)          # JSONDecodeError
ast.literal_eval(s)    # ✓ {'a': 1, 'b': True}

# 场景2:含 tuple 的字符串
s = "(1, 2, 3)"
json.loads(s)          # JSONDecodeError
ast.literal_eval(s)    # ✓ (1, 2, 3)

# 场景3:含 None(JSON 是 null)
json.loads('{"x": null}')    # ✓ {'x': None}
json.loads('{"x": None}')    # JSONDecodeError
ast.literal_eval('{"x": None}')  # ✓ {'x': None}

# 场景4:高性能 JSON
import ujson
ujson.loads('{"a": 1}')  # ✓ 最快

总结

优先用 json.loads ,不能用就 ast.literal_eval ,出现性能瓶颈就上 ujson.loads ,永远不要用 eval

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy