ijson kvitems 意外行为

ijson kvitems unexpected behaviour

我正在使用 ijson 来解析大型 JSON。我有这段代码,它应该给我一个 dict 对应于相关 JSON 字段的值:

def parse_kvitems(kv_gen, key_list):
    results = {}
    for key in key_list:
        results[key] = (v for k, v in kv_gen if k == key)
    return results

with zipfile.ZipFile(fr'{directory}\{file}', 'r') as zipObj:
    # Get a list of all archived file names from the zip
    listOfFileNames = zipObj.namelist()
    # Iterate over the file names
    for fileName in listOfFileNames:
        # Check filename endswith csv.  dont extract, ijson wants bytes input and json.loads can run into memory issues with smash jsons.No documentation available 
        if fileName.endswith('.json'):
            # Extract a single file from zip
            with zipObj.open(fileName) as f:

                #HERE:
                records = ijson.kvitems(f, 'records.item')
                data_list = ['id', 'features', 'modules', 'dbxrefs', 'description']    
                parsed_records = parse_kvitems(records, data_list) --> give me a dict of dict values that fall under the json headings in data_list

我认为 kvitems 对象就像一个生成器,只通过一个 运行-through(我得到 'id' 的预期值,但其他 data_listparsed_records 中为空)。

为了解决这个问题,我尝试列出重复的 kv_gen

def parse_kvitems(kv_gen, key_list):
    kv_list = [kv_gen] * len(key_list) #this bit
    results = {}
    for key, kv_gen in zip(key_list, kv_list):
        results[key] = (v for k, v in kv_gen if k == key)
    return results

这给了我同样的错误。我认为可变性可能是这里的罪魁祸首,但我不能在 kvitems 对象上使用 copy 来查看这是否修复了它。

然后我尝试使用 itertools.cycle(),但这似乎以我不理解的方式工作:

def parse_kvitems(kv_gen, key_list):
    infinite_kvitems = itertools.cycle(kv_gen)
    results = {}
    for key in key_list:
        results[key] = (v for k, v in infinite_kvitems if k == key)
    return results

此外,下面的代码有效(从某种意义上说,它给我的值与我用 json.load() 加载 JSON 时看到的值相匹配):

records = ijson.kvitems(f, 'records.item')
ids = (v for k, v in records if k == 'id')
features = (v for k, v in records if k == 'features')
modules = (v for k, v in records if k == 'modules')

我只是想知道为什么我的函数没有,特别是当记录对象被 运行 以上多次...


为罗德里戈编辑

You are not showing however how you find that your final dictionary has values for id but not for the other keys. I'm assuming it's only because you are iterating over the values under the parse_records['id'] values first. As you do so, the generator expression is then evaluated and the underlying kvitems generator is exhausted.

是的,这是正确的 - 我正在将每个 val 转换为一个列表以检查每个键是否有一个包含相同数量项目的生成器,因为我担心下游 zip 操作可能会 运行cate 一些如果他们有比最小生成器更多的对象的值。

我没有转换为函数中的列表,因为我认为生成器是 return 的更好对象(较少内存密集型等),然后我可以将其转换为我需要的列表到函数外。

You say that your finally piece of code works as expected. This is the only bit that surprises me, specially if you really, really inspected (i.e., evaluated) all three of the generator expressions after you created them. If you could clarify if that's the case it would be interesting; otherwise if you created all three generator expressions, but then evaluated one or the other, then there are no surprises here (because of the "About result collection" explanation).

基本上,当我 运行 将项目作为生成器的压缩集合并将项目附加到列表时,它给了我预期的值。但这可能需要更多调查,JSON 非常复杂,所以我可能漏掉了一些东西。

关于结果收集

注意您如何从 kvitems 收集结果。在上面的所有示例中,您都使用了生成器表达式,它们本身是惰性求值的,这可能会导致误解。然而,您没有显示 如何 您发现您的最终字典具有 id 的值,但没有其他键的值。我假设这只是因为您正在迭代 parse_records['id']first 下的值。当您这样做时,生成器表达式将被求值,底层的 kvitems 生成器将被耗尽。当您迭代其他生成器表达式的值时,为它们提供数据的底层 kvitems 生成器已耗尽,因此它们什么也不会产生。但是,如果您要遍历其他键之一的值 first,您应该看到该键的值,而不是其他键的值。

生成器表达式本身很棒,但在这种情况下,它可能最终会增加混乱。 如果您想避免这种情况,您可能希望将这些序列合并为列表(例如,使用 [... for k, v in kvitems ...] 而不是 (... for k, v in kvitems ...))。

关于 kvitems

正如您所指出的,kvitems 是一个单程生成器(或者当输入异步文件类对象时是一个单程异步生成器),所以一旦您完全迭代它,进一步的迭代不产生任何价值。这就是为什么在您的原始代码中您确实获得了 id 的值,但没有获得在已迭代的 kvitems 对象的后续迭代中收集的其他键的值。

尝试复制 kvitems 对象也是伪造的:正如您还发现的那样,您只是在创建一个列表,其中所有位置都包含 相同的 对象,而不是原始对象的副本。

尝试 copy kvitems 根本不可能。获得 N 个“副本”的唯一选择是实际构造 N 个不同的对象;然而,这意味着输入文件将被读取 N 次(并且也需要打开 N 次,因为 kvitems 将推进给定的文件,直到它没有更多的输入)。可能,但不是很好。

itertools.cycle 的结果是无限生成器。然后你以此为基础构造不同的生成器表达式(因此,惰性求值)。你提到这个解决方案以“你不理解”的方式工作,但不要深入研究到底发生了什么。我的期望是,当尝试检查任何键的值时,您 运行 进入无限循环,因为您的生成器表达式正在迭代无限生成器或类似的东西。

你说你的最后一段代码按预期工作。这是唯一让我感到惊讶的一点,特别是如果您在创建生成器表达式后真的非常检查(即评估)所有三个生成器表达式。如果您能澄清情况是否如此,那将很有趣;否则,如果您创建了所有三个生成器表达式,但随后对其中一个进行了求值,那么这里就没有什么奇怪的了(因为“关于结果集合”的解释)。

如何解决您的问题

基本上都归结为对 kvitems 进行一次迭代。例如,您可以尝试这样的事情:

def parse_kvitems(kvitems, keys):
    results = collections.defaultdict(list)
    for k, v in kvitems:
        if k in keys:
            results[k].append(v)
    return results

我认为应该这样做。