性能2-使用 msgspec 实现更快、更高效内存的 Python JSON 解析

目录

使用 msgspec 实现更快、更高效内存的 Python JSON 解析

如果你需要在 Python 中处理大型 JSON 文件,你可能希望:

  1. 确保不会使用过多内存,以免在处理过程中崩溃。
  2. 尽可能快地解析它。
  3. 理想情况下,确保数据在前期是有效的,具有正确的结构,以免在分析过程中出错。

当然,你可以使用多个库来组合解决方案。或者,你可以使用 msgspec,这是一个新的库,提供了模式、快速解析和一些减少内存使用的巧妙技巧,所有这些都在一个库中。

起点:内置的 jsonorjson

让我们先看看两个其他库:Python 内置的 json 模块和快速的 orjson 库。具体来说,我们将解析一个约 25MB 的文件,该文件编码了一个 JSON 对象列表(即字典),这些对象看起来像是 GitHub 事件,用户对仓库的操作:

[{"id":"2489651045","type":"CreateEvent","actor":{"id":665991,"login":"petroav","gravatar_id":"","url":"https://api.github.com/users/petroav","avatar_url":"https://avatars.githubusercontent.com/u/665991?"},"repo":{"id":28688495,"name":"petroav/6.828","url":"https://api.github.com/repos/petroav/6.828"},"payload":{"ref":"master","ref_type":"branch","master_branch":"master","description":"Solution to homework and assignments from MIT's 6.828 (Operating Systems Engineering). Done in my spare time.","pusher_type":"user"},"public":true,"created_at":"2015-01-01T15:00:00Z"},
...
]

我们的目标是找出某个用户与哪些仓库进行了交互。

以下是使用 Python 标准库内置的 json 模块的实现:

import json

with open("large.json", "r") as f:
    data = json.load(f)

user_to_repos = {}
for record in data:
    user = record["actor"]["login"]
    repo = record["repo"]["name"]
    if user not in user_to_repos:
        user_to_repos[user] = set()
    user_to_repos[user].add(repo)
print(len(user_to_repos), "records")

以下是使用 orjson 的实现,只需两行更改:

import orjson

with open("large.json", "rb") as f:
    data = orjson.loads(f.read())

user_to_repos = {}
for record in data:
    # ... 与标准库代码相同 ...

以下是这两种方法的内存和时间消耗:

$ /usr/bin/time -f "RAM: %M KB, Elapsed: %E" python stdlib.py 
5250 records
RAM: 136464 KB, Elapsed: 0:00.42
$ /usr/bin/time -f "RAM: %M KB, Elapsed: %E" python with_orjson.py 
5250 records
RAM: 113676 KB, Elapsed: 0:00.28

内存使用相似,但 orjson 更快,耗时 280ms 而不是 420ms。

接下来,我们来看看 msgspec

msgspec:基于模式的 JSON 解码和编码

以下是使用 msgspec 的相应代码;正如你所看到的,它在解析方法上有所不同:

from msgspec.json import decode
from msgspec import Struct

class Repo(Struct):
    name: str

class Actor(Struct):
    login: str

class Interaction(Struct):
    actor: Actor
    repo: Repo

with open("large.json", "rb") as f:
    data = decode(f.read(), type=list[Interaction])

user_to_repos = {}
for record in data:
    user = record.actor.login
    repo = record.repo.name
    if user not in user_to_repos:
        user_to_repos[user] = set()
    user_to_repos[user].add(repo)
print(len(user_to_repos), "records")

这段代码更长,更详细,因为 msgspec 允许你为要解析的记录定义模式。

非常有用的是,你不必为所有字段都定义模式。 虽然 JSON 记录有很多字段(参见前面的示例以查看所有数据),但我们只告诉 msgspec 我们实际关心的字段。

以下是使用 msgspec 解析的结果:

$ /usr/bin/time -f "RAM: %M KB, Elapsed: %E" python with_msgspec.py 
5250 records
RAM: 38612 KB, Elapsed: 0:00.09

更快,内存使用更少。

总结一下我们看到的三种选项,以及一个基于 流式 ijson 的解决方案

时间内存固定内存使用模式
标准库 json420ms136MB
orjson280ms114MB
ijson300ms14MB
msgspec90ms39MB

流式解决方案在解析过程中只使用固定数量的内存;其他解决方案的内存使用与输入大小成比例。但在这三种方案中,msgspec 的内存使用显著更低,而且它是迄今为止最快的解决方案。

基于模式解析的优缺点

由于 msgspec 允许你指定模式,我们能够仅为实际关心的字段创建 Python 对象。这意味着更低的内存使用和更快的解码;不需要浪费时间和内存创建数千个我们永远不会查看的 Python 对象。

我们还免费获得了模式验证。如果某个记录缺少字段,或者值类型错误(例如整数而不是字符串),解析器会报错。使用标准 JSON 库,模式验证必须单独进行。

另一方面:

  • 解码时的内存使用仍然与输入文件成比例。像 ijson 这样的流式 JSON 解析器仍然提供在解析过程中固定内存使用的优势,无论输入文件有多大。
  • 指定模式需要更多的编码工作,并且处理不完美数据的灵活性较低。
了解更多关于 msgspec 的信息

msgspec 还有其他功能,如编码、MessagePack 支持(一种比 JSON 更快的替代格式)等。如果你经常解析 JSON 文件,并且遇到性能或内存问题,或者你只是想要内置的模式,考虑试试它。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李星星BruceL

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值