如何在Python中懒惰地读取文件/流中的多个JSON值?

117 投票
13 回答
101186 浏览
提问于 2025-04-16 22:35

我想在Python中从一个文件或流中逐个读取多个JSON对象。可惜的是,json.load()方法会一直.read()到文件结束;似乎没有办法用它来读取单个对象或者懒惰地遍历这些对象。

有没有什么办法可以做到这一点?如果能用标准库那就最好了,不过如果有第三方库的话,我也会考虑使用。

目前我把每个对象放在单独的一行,然后用json.loads(f.readline())来读取,但我真的希望不需要这样做。

示例用法

example.py

import my_json as json
import sys

for o in json.iterload(sys.stdin):
    print("Working on a", type(o))

in.txt

{"foo": ["bar", "baz"]} 1 2 [] 4 5 6

示例会话

$ python3.2 example.py < in.txt
Working on a dict
Working on a int
Working on a int
Working on a list
Working on a int
Working on a int
Working on a int

13 个回答

37

可能有点晚了,但我遇到过完全一样的问题(或者说差不多)。我通常解决这类问题的方法是对一些常见的根对象进行正则表达式分割,但在我的情况下,这样做是不可能的。唯一可行的通用方法就是实现一个合适的分词器

在找不到一个足够通用且性能合理的解决方案后,我最终自己动手写了一个,叫做splitstream模块。这个模块是一个预处理的分词器,能够理解JSON和XML,并将连续的数据流分割成多个部分以便解析(不过实际的解析工作还是得你自己来做)。为了提高性能,它是用C语言编写的。

示例:

from splitstream import splitfile

for jsonstr in splitfile(sys.stdin, format="json")):
    yield json.loads(jsonstr)
45

JSON在逐步使用时通常不是很理想,因为没有标准的方法来把多个对象序列化,这样就不能轻松地一个一个加载,而不需要解析所有内容。

你正在使用的每行一个对象的解决方案在其他地方也有类似的用法。Scrapy把它称为“JSON行”:

你可以用更符合Python风格的方式来做:

for jsonline in f:
    yield json.loads(jsonline)   # or do the processing in this loop

我觉得这就是最好的方法了——它不依赖任何第三方库,而且很容易理解发生了什么。我在我自己的代码中也用过这个方法。

26

这里有一个简单得多的解决方案。关键在于尝试、失败,然后利用错误信息来正确解析。唯一的限制是文件必须是可寻址的。

def stream_read_json(fn):
    import json
    start_pos = 0
    with open(fn, 'r') as f:
        while True:
            try:
                obj = json.load(f)
                yield obj
                return
            except json.JSONDecodeError as e:
                f.seek(start_pos)
                json_str = f.read(e.pos)
                obj = json.loads(json_str)
                start_pos += e.pos
                yield obj

补充:我刚注意到这个方法只适用于Python 3.5及以上版本。对于更早的版本,失败时会返回一个ValueError,你需要从字符串中解析出位置,比如:

def stream_read_json(fn):
    import json
    import re
    start_pos = 0
    with open(fn, 'r') as f:
        while True:
            try:
                obj = json.load(f)
                yield obj
                return
            except ValueError as e:
                f.seek(start_pos)
                end_pos = int(re.match('Extra data: line \d+ column \d+ .*\(char (\d+).*\)',
                                    e.args[0]).groups()[0])
                json_str = f.read(end_pos)
                obj = json.loads(json_str)
                start_pos += end_pos
                yield obj

撰写回答