使用 ijson.parse() 和 ijson.items() 加载一个大的 JSON 文件 - 为什么这样做有效?

Using ijson.parse() and ijson.items() to load a big JSON file - why does this work?

我正在尝试加载 JSON 个对 json.load 来说太大的文件。我花了一段时间研究 ijson 和许多堆栈溢出帖子,并使用了以下代码,大部分是从 偷来的:

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

为了实现这一点,我研究了使用 ijson.parse 来给出前缀列表。当我使用迭代循环将下面奇怪的生成器 parser 对象制作的所有前缀输入 ijson.items() 时,我很快从 json.items() 语句中得到了 MemoryError 。我在代码的早期迭代中也得到了 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

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

如您所知,我对使用 JSON 非常不熟悉,所以任何通用 tips/clarifications 也很好。

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

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

why is the records prefix necessary for the the extract_json function ...?

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,它应该产生什么对象?应该是:

  • 123,或
  • ABC,或
  • {"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,或
  • [123],或["A", "B", "C"],或[{"i": 10, "j": 20, "k": 30}, ...],或
  • 完整的对象,或者....

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

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

并且您想 return 该列表的值作为单独的对象。

如果我没看错你的问题,我认为你的根本问题是 ijson.items 对单个前缀进行操作,但你想从不同的前缀中提取对象。此功能尚未在 ijson 中,但实际上可以添加(我认为应该不会太难)。一个类似的想法是支持“通配符”(例如,看起来像 *.items 的前缀),我认为也可以支持。

话虽如此,请看一下kvitems函数。它 returns key, value 对给定的前缀,这听起来或多或少像你需要的。

(I tried the other keys in the json dictionary, there were no hits)

如果您可以分享 JSON 文件的摘录或简化示例,可以对其进行评论。

... but counterproductive for loadBigJson?

因为loadBigJsonBADloadBigJson都有缺陷

首先,他们都使用未重置的文件多次调用 ijson.parse。第一次调用会工作,但会耗尽文件对象(即,read 将 return 什么都没有。进一步的调用使用这个耗尽的文件对象并失败,因为没有任何内容可读,因此它们引发 IncompleteJSON错误。

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

What is generating the Error statements and why does an except IncompleteJSONError statement prevent a MemoryError?

你在哪里得到一个MemoryError,经过多少时间?我能想到的 only 可能性是你正在使用 3.0.0 <= ijson < 3.1.2 与 yajl2_c 后端,并且你正在泄漏通过创建太多 ijson.items 对象(参见 this bug report)来占用内存。但是,如果您首先使用 set 来存储前缀,那么这可能不会发生。

Thanks for reading the novel

不客气!

此外,请注意,不要循环遍历 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) 对,这将迭代地完成,只使用保存该特定对所需的内存。如果您仍想将所有内容放入一个列表中,您可以这样做,但请注意,对于大文件,您可能 运行 内存不足(这就是使用 ijson v/s json,对吧?)

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

关于“无效字符”错误,是的,这可能是一个后续问题。我只是在这里冒险猜测,但是如果您 copy/pasted 来自 JSON 生成器 link 的 JSON 内容到编辑器中并保存该文件,那么它很可能已被编码使用本地默认编码而不是 UTF8,这就是产生错误的原因。我尝试使用“下载 JSON 文件”选项,效果很好。

(注意 - 回答是因为这远远超过了字符数限制

啊,谢谢你,尤其是 ijson 正在做的事情的具体细节——我已经为此苦苦思索了一段时间! 您对 extract_json 的评论很到位,代码更易读。对于 JSON 结构,我看不到附加文件的选项,但它的格式为:

{
key 1: ”string”,
“records”: [list with lots of nested dictionary/list objects]
key3: int
key4: string
key5: {dict with nested dictionary objects}
key6: str
}

‘records’ 有我想要的大部分信息,但我将其视为学习练习,所以想获得所有信息。你对其他 2 个函数的问题也是正确的,让我有机会修改文件在 python 中的工作方式!是的,有很多(很多)前缀,甚至 set() 也为 ijson.items 列出了一个很长的列表。我查看了前缀,并决定将它们缩减为 <1 ‘.’ 即如果我理解正确的话,初始密钥。当合并到下面的代码中时,它可以正常工作,没有错误。

def NewJsonLoad(filename):
    with open(filename, 'rb') as input_file:
        #get all prefixes in json file
        prefixes=[]
        parser = ijson.parse(input_file)
        for prefix , event, value in parser:
                prefixes.append(prefix)
        prefixes = list(set(prefixes))
        prefixes_filtered=[]
        
        #pull out prefixes that are the initial keys only
        for prefix in prefixes:
            if prefix.count('.')==0:
                prefixes_filtered.append(prefix)
        
        #pull out items for the filtered prefixes
        finalout=[]
        for prefix in prefixes_filtered:
            input_file.seek(0)#reset pointer - see 
            jsonobjn = ijson.items(input_file, prefix, use_float=True)
            jsonsn = (o for o in jsonobjn)
            for jn in jsonsn:
                finalout.append(jn)
    return finalout[0]#feeding jn into a list object, not the original dict object - this is item [0]

您认为这行得通并且可以推广,还是我遗漏了一些更明显的错误?

我试图找到一些可以玩的虚拟 csv (https://www.json-generator.com/),以测试它是否适用于生成 JSON 文件的程序未格式化的文件一起工作。然而,出于某种原因,我的函数和 json.load 都不喜欢它——可能与解码有关,我看到这个术语有点乱来:P

IncompleteJSONError: lexical error: invalid char in json text.
                                       [    {      "_id": "5f80c3b4
                     (right here) ------^ 

为help/tutorial干杯!