首页 > 解决方案 > 使用 ijson.parse() 和 ijson.items() 加载一个大的 JSON 文件 - 为什么这样会起作用?

问题描述

我正在尝试加载对于json.load. 我花了一段时间研究了ijson许多堆栈溢出帖子,并使用了以下代码,大部分是从https://stackoverflow.com/a/58148422/11357695窃取的:

def extract_json(filename):
    listJ=[]
    with open(filename, 'rb') as input_file:
        jsonobj = ijson.items(input_file, 'records.item', use_float=True)
        jsons = (o for o in jsonobj)
        for j in jsons:
            listJ.append(j)
    return listJ

我的 JSON 文件作为 dict 读入,有 6 个键,其中一个是'records'. 上述函数仅复制此'records'键值的内容。我对此进行了更多研究,并得出ijson.items使用前缀 ( 'records.item') 的结论。所以它只是复制这个键的值也就不足为奇了。但我想得到一切。

为了实现这一点,我查看了 usingijson.parse来给出前缀列表。当我将parser下面奇怪的生成器对象生成的所有前缀输入到ijson.items()使用迭代循环中时,我MemoryError很快就从json.items()语句中得到了一个。我还获得IncompleteJSONError了代码的早期迭代,当前版本没有出现。但是,如果我删除该except ijson.IncompleteJSONError语句,我会得到Memory Error

def loadBigJsonBAD(filename):
    with open(filename, 'rb') as input_file:
        parser = ijson.parse(input_file)
        prefixes=[]
        for prefix , event, value in parser:
            prefixes.append(prefix)
    listJnew=[]
    with open(filename, 'rb') as input_file:
        for prefix in prefixes:
            jsonobjn = ijson.items(input_file, prefix, use_float=True)
            try:
                jsonsn = (o for o in jsonobjn)
                for jn in jsonsn:
                    listJnew.append(jn)
            except ijson.IncompleteJSONError:
                continue
    return listJnew

我尝试了如果我只搜索没有 的前缀会发生什么'record',看看这是否至少会给我字典的其余部分。但是,它实际上工作得很好,并创建了一个列表,其第一个对象与生成的对象相同json.load(在这种情况下它工作,因为我使用一个小文件来测试代码):

def loadBigJson(filename):
    with open(filename, 'rb') as input_file:
        parser = ijson.parse(input_file)
        prefixes=[]
        for prefix , event, value in parser:
            if prefix[0:len('records')] != 'records':
                prefixes.append(prefix)
    listJnew=[]
    with open(filename, 'rb') as input_file:
        for prefix in prefixes:
            jsonobjn = ijson.items(input_file, prefix, use_float=True)
            try:
                jsonsn = (o for o in jsonobjn)
                for jn in jsonsn:
                    listJnew.append(jn)
            except ijson.IncompleteJSONError:
                continue
    return listJnew

测试时:

path_json=r'C:\Users\u03132tk\.spyder-py3\antismashDB\GCF_010669165.1\GCF_010669165.1.json'

extractedJson=extract_json(path_json) #extracts the 'records' key value

loadedJson=json.load(open(path_json, 'r'))  #extracts entire json file
loadedJsonExtracted=loadedJson['records']   #the thing i am using to compare to the extractedJson item

bigJson=loadBigJson(path_json)  #a list whose single object is the same as loaded json. 

print (bigJson[0]==loadedJson)#True
print (bigJson[0]['records']==loadedJsonExtracted)#True
print (bigJson[0]['records']==extractedJson)#True

这很好,但它强调我并不真正了解发生了什么 - 为什么records该函数需要前缀extract_json(我尝试了 json 字典中的其他键,没有命中)但适得其反loadBigJson?是什么生成了错误语句,为什么except IncompleteJSONError语句会阻止MemoryError

如您所知,我对使用 JSON 非常不熟悉,因此任何一般性提示/说明也会很棒。

感谢您阅读小说,即使您没有答案!
蒂姆

标签: jsonpython-3.7ijson

解决方案


提出了几个问题,所以我将尝试将它们分解一下。

为什么recordsextract_json 函数需要前缀...?

ijson需要知道什么时候应该开始构建对象。请记住,您提供ijson了一个数据流,因此它永远不知道您文档的完整结构。这意味着如果没有这个提示ijson,就无法猜出你的意图,或者靠自己的力量想出一个。

说你有

{
  "a": [1, 2, 3],
  "b": ["A", "B", "C"],
  "c": [{"i": 10, "j": 20, "k": 30},
        {"i": 11, "j": 21, "k": 31},
        {"i": 12, "j": 22, "k": 32}]
}

如果你把它交给ijson.items,它应该产生什么对象?应该是:

  • 1,23, 或
  • A,BC, 或
  • {"i": 10, "j": 20, "k": 30},{"i": 11, "j": 21, "k": 31}{"i": 12, "j": 22, "k": 32}, 或
  • 10, 20, 30, 11, 21, 31, 12, 22, 和32, 或
  • [ 1, 2, 3] 或["A", "B", "C"], 或[{"i": 10, "j": 20, "k": 30}, ...], 或
  • 完整的对象,或....

构建哪些对象items取决于您给它的前缀。如果您提供ijson前缀records.item,则意味着您有一个 JSON 文档,如下所示:

{
  ...
  "records": [.....],
  ...
}

并且您希望将该列表的值作为单个对象返回。

如果我在您问题的两行之间正确阅读,我认为您遇到的根本问题是ijson.items对单个前缀进行操作,但您想从不同的前缀中提取对象。这个功能还没有ijson,但实际上可以添加(我认为应该不会太难)。一个类似的想法是支持“通配符”(例如,看起来像的前缀*.items),我认为也可以支持。

话虽如此,请看一下kvitems功能。它返回key, value给定前缀的对,这听起来或多或少像您需要的那样。

(我尝试了 json 字典中的其他键,没有命中)

如果您可以分享您的 JSON 文件的摘录或简化示例,可以对此发表评论。

...但对 loadBigJson 会适得其反吗?

因为两者loadBigJsonBAD都有loadBigJson缺陷。

首先,它们都ijson.parse使用未重置的文件多次调用。第一次调用将起作用,但会耗尽文件对象(即,read不会返回任何内容。进一步的调用会使用这个耗尽的文件对象并失败,因为没有可读取的内容,因此它们会引发 IncompleteJSONError。

其次,为 JSON 文档中的每个事件ijson.parse生成一个prefix,key,value元组:对象何时开始,对象何时结束,数组何时开始和结束,以及何时找到原子值(字符串、数字、布尔值)。将所有这些前缀累积到一个列表中会给您提供比您需要的条目更多的条目,其中许多条目将被重复。您至少应该将它们放入一个集合中;否则你就是在重复自己。

是什么产生了错误语句,为什么除了 IncompleteJSONError 语句会阻止 MemoryError?

你在哪里得到一个MemoryError,经过多长时间?我能想到的唯一可能性是您ijson在后端使用 3.0.0 <= < 3.1.2 yajl2_c,并且您通过创建太多 ijson.items 对象来泄漏内存(请参阅此错误报告)。set但是,如果您首先使用 a来存储前缀,这可能不会发生。

感谢阅读小说

别客气!

此外,请注意,与其循环遍历结果items并将值附加到列表中(也就是说,如果您真的,真的想一次收集内存中的所有对象),您应该能够直接从迭代器。所以而不是:

def extract_json(filename):
    listJ=[]
    with open(filename, 'rb') as input_file:
        jsonobj = ijson.items(input_file, 'records.item', use_float=True)
        jsons = (o for o in jsonobj)
        for j in jsons:
            listJ.append(j)
    return listJ

你应该能够做到:

def extract_json(filename):
    with open(filename, 'rb') as input_file:
        return list(ijson.items(input_file, 'records.item', use_float=True))

编辑1:

对于给定 JSON 示例的结构,您可能希望kvitems使用空前缀。所以:

for key, value in ijson.kvitems(input_file, ''):
    # (key, value) will be:
    #  (key1, ”string”),
    #  ("records", [list with lots of nested dictionary/list objects])
    #  ("key3", int)
    #  ("key4", string)
    #  ("key5", {dict with nested dictionary objects})
    #  ("key6", str)

这听起来与您正在努力实现的目标完全一样,并且已经为您完成了所有工作。每次迭代都会给你一个不同的(key, value)对,这将迭代地完成,只需要使用尽可能多的内存来保存那个特定的对。如果您仍想将所有内容放入一个列表中,您可以这样做,但请注意,对于大文件,您可能会耗尽内存(这就是使用ijsonv/s的目的json,对吧?)

所以是的,你的新代码是通用的,但是:1)如果你使用我认为它可以变得更简单kvitems,2)它有点违背了遍历大文件的目的,因为你仍然在内存中累积所有内容。

关于“无效字符”错误,是的,这可能是一个问题。我只是在这里冒险猜测,但是如果您将 JSON 生成器链接中的 JSON 内容复制/粘贴到编辑器中并保存该文件,则很可能它是以您的本地默认编码而不是 UTF8 编码的,这就是产生的错误。我尝试使用“下载 JSON 文件”选项,效果很好。


推荐阅读