【问题标题】:Flatten nested JSON (Dict, List) into List to prepare to write into DB将嵌套的 JSON (Dict, List) 展平成 List 以准备写入 DB
【发布时间】:2016-08-12 09:42:36
【问题描述】:

我仍在处理一个扁平化嵌套 JSON 文件的问题。嵌套项是 List 或 Dict:

这是我想要展平的文件(与我之前的帖子不同,我保留了它的长度,但它只包含 input[0] 不包含任何后续项目,因为它会很长):

input = [{'states': ['USED'], 'niceName': '1-series', 'id': 'BMW_1_Series',
            'years': [{'styles':
                       [{'trim': '128i', 'states': ['USED'], 'submodel': {'body': 'Convertible', 'niceName': 'convertible', 'modelName': '1 Series Convertible'},
                         'name': '128i 2dr Convertible (3.0L 6cyl 6M)', 'id': 100994560},
                        {'trim': '128i', 'states': ['USED'], 'submodel': {'body': 'Coupe', 'niceName': 'coupe', 'modelName': '1 Series Coupe'},
                          'name': '128i 2dr Coupe (3.0L 6cyl 6M)', 'id': 100974974},
                        {'trim': '135i', 'states': ['USED'], 'submodel': {'body': 'Coupe', 'niceName': 'coupe', 'modelName': '1 Series Coupe'}, 
                         'name': '135i 2dr Coupe (3.0L 6cyl Turbo 6M)', 'id': 100974975},
                        {'trim': '135i', 'states': ['USED'], 'submodel': {'body': 'Convertible', 'niceName': 'convertible', 'modelName': '1 Series Convertible'}, 
                         'name': '135i 2dr Convertible (3.0L 6cyl Turbo 6M)', 'id': 100994561}
                        ],
                       'states': ['USED'], 'id': 100524709, 'year': 2008},
                      {'styles':
                       [{'trim': '135i', 'states': ['USED'], 'submodel': {'body': 'Coupe', 'niceName': 'coupe', 'modelName': '1 Series Coupe'}, 
                         'name': '135i 2dr Coupe (3.0L 6cyl Turbo 6M)', 'id': 101082656}, 
                        {'trim': '128i', 'states': ['USED'], 'submodel': {'body': 'Coupe', 'niceName': 'coupe', 'modelName': '1 Series Coupe'}, 
                         'name': '128i 2dr Coupe (3.0L 6cyl 6M)', 'id': 101082655},
                        {'trim': '135i', 'states': ['USED'], 'submodel': {'body': 'Convertible', 'niceName': 'convertible', 'modelName': '1 Series Convertible'}, 
                         'name': '135i 2dr Convertible (3.0L 6cyl Turbo 6M)', 'id': 101082663},
                        {'trim': '128i', 'states': ['USED'], 'submodel': {'body': 'Convertible', 'niceName': 'convertible', 'modelName': '1 Series Convertible'}, 
                         'name': '128i 2dr Convertible (3.0L 6cyl 6M)', 'id': 101082662}
                        ], 
                       'states': ['USED'], 'id': 100503222, 'year': 2009},
                      {'styles': 
                       [{'trim': '128i', 'states': ['USED'], 'submodel': {'body': 'Coupe', 'niceName': 'coupe', 'modelName': '1 Series Coupe'}, 
                         'name': '128i 2dr Coupe (3.0L 6cyl 6M)', 'id': 101200599},
                        {'trim': '135i', 'states': ['USED'], 'submodel': {'body': 'Coupe', 'niceName': 'coupe', 'modelName': '1 Series Coupe'}, 
                         'name': '135i 2dr Coupe (3.0L 6cyl Turbo 6M)', 'id': 101200600}, 
                        {'trim': '135i', 'states': ['USED'], 'submodel': {'body': 'Convertible', 'niceName': 'convertible', 'modelName': '1 Series Convertible'}, 
                         'name': '135i 2dr Convertible (3.0L 6cyl Turbo 6M)', 'id': 101200607}, 
                        {'trim': '128i', 'states': ['USED'], 'submodel': {'body': 'Convertible', 'niceName': 'convertible', 'modelName': '1 Series Convertible'}, 
                         'name': '128i 2dr Convertible (3.0L 6cyl 6M)', 'id': 101200601}
                        ], 
                       'states': ['USED'], 'id': 100529091, 'year': 2010}, 
                      {'styles':
                       [{'trim': '128i', 'states': ['USED'], 'submodel': {'body': 'Coupe', 'niceName': 'coupe', 'modelName': '1 Series Coupe'}, 
                         'name': '128i 2dr Coupe (3.0L 6cyl 6M)', 'id': 101288165}, 
                        {'trim': '135i', 'states': ['USED'], 'submodel': {'body': 'Coupe', 'niceName': 'coupe', 'modelName': '1 Series Coupe'}, 
                         'name': '135i 2dr Coupe (3.0L 6cyl Turbo 6M)', 'id': 101288166}, 
                        {'trim': '135i', 'states': ['USED'], 'submodel': {'body': 'Convertible', 'niceName': 'convertible', 'modelName': '1 Series Convertible'}, 
                         'name': '135i 2dr Convertible (3.0L 6cyl Turbo 6M)', 'id': 101288298}, 
                        {'trim': '128i', 'states': ['USED'], 'submodel': {'body': 'Convertible', 'niceName': 'convertible', 'modelName': '1 Series Convertible'}, 
                         'name': '128i 2dr Convertible (3.0L 6cyl 6M)', 'id': 101288297}
                        ], 
                       'states': ['USED'], 'id': 100531309, 'year': 2011}, 
                      {'styles': 
                       [{'trim': '128i', 'states': ['USED'], 'submodel': {'body': 'Convertible', 'niceName': 'convertible', 'modelName': '1 Series Convertible'}, 
                         'name': '128i 2dr Convertible (3.0L 6cyl 6M)', 'id': 101381667}, 
                        {'trim': '135i', 'states': ['USED'], 'submodel': {'body': 'Convertible', 'niceName': 'convertible', 'modelName': '1 Series Convertible'}, 
                         'name': '135i 2dr Convertible (3.0L 6cyl Turbo 6M)', 'id': 101381668}, 
                        {'trim': '128i', 'states': ['USED'], 'submodel': {'body': 'Coupe', 'niceName': 'coupe', 'modelName': '1 Series Coupe'}, 
                         'name': '128i 2dr Coupe (3.0L 6cyl 6M)', 'id': 101381665}, 
                        {'trim': '135i', 'states': ['USED'], 'submodel': {'body': 'Coupe', 'niceName': 'coupe', 'modelName': '1 Series Coupe'}, 
                         'name': '135i 2dr Coupe (3.0L 6cyl Turbo 6M)', 'id': 101381666}
                        ], 
                       'states': ['USED'], 'id': 100534729, 'year': 2012}, 
                      {'styles': 
                       [{'trim': '128i', 'states': ['USED'], 'submodel': {'body': 'Coupe', 'niceName': 'coupe', 'modelName': '1 Series Coupe'}, 
                        'name': '128i 2dr Coupe (3.0L 6cyl 6M)', 'id': 200428722},
                        {'trim': '128i', 'states': ['USED'], 'submodel': {'body': 'Convertible', 'niceName': 'convertible', 'modelName': '1 Series Convertible'}, 
                         'name': '128i 2dr Convertible (3.0L 6cyl 6M)', 'id': 200428721}, 
                        {'trim': '135is', 'states': ['USED'], 'submodel': {'body': 'Coupe', 'niceName': 'coupe', 'modelName': '1 Series Coupe'}, 
                         'name': '135is 2dr Coupe (3.0L 6cyl Turbo 6M)', 'id': 200421701}, 
                        {'trim': '135i', 'states': ['USED'], 'submodel': {'body': 'Coupe', 'niceName': 'coupe', 'modelName': '1 Series Coupe'}, 
                         'name': '135i 2dr Coupe (3.0L 6cyl Turbo 6M)', 'id': 200428724}, 
                        {'trim': '135i', 'states': ['USED'], 'submodel': {'body': 'Convertible', 'niceName': 'convertible', 'modelName': '1 Series Convertible'}, 
                         'name': '135i 2dr Convertible (3.0L 6cyl Turbo 6M)', 'id': 200428723}, 
                        {'trim': '128i SULEV', 'states': ['USED'], 'submodel': {'body': 'Coupe', 'niceName': 'coupe', 'modelName': '1 Series Coupe'}, 
                         'name': '128i SULEV 2dr Coupe (3.0L 6cyl 6M)', 'id': 200428726}, 
                        {'trim': '128i SULEV', 'states': ['USED'], 'submodel': {'body': 'Convertible', 'niceName': 'convertible', 'modelName': '1 Series Convertible'}, 
                         'name': '128i SULEV 2dr Convertible (3.0L 6cyl 6M)', 'id': 200428725}, 
                        {'trim': '135is', 'states': ['USED'], 'submodel': {'body': 'Convertible', 'niceName': 'convertible', 'modelName': '1 Series Convertible'}, 
                         'name': '135is 2dr Convertible (3.0L 6cyl Turbo 6M)', 'id': 200428727}
                        ], 
                       'states': ['USED'], 'id': 200421700, 'year': 2013}
                      ], 
          'name': '1 Series', 'make': {'niceName': 'bmw', 'name': 'BMW', 'id': 200000081}
          }, #here is more to come, but I needed to crop it
          ]

在我的方法失败后我使用的代码是由@poke 编写的,来自:Flattening Generic JSON List of Dicts or Lists in Python

def splitObj (obj, prefix = None):
    '''
    Split the object, returning a 3-tuple with the flat object, optionally
    followed by the key for the subobjects and a list of those subobjects.
    '''
    # copy the object, optionally add the prefix before each key
    new = obj.copy() if prefix is None else { '{}_{}'.format(prefix, k): v for k, v in obj.items() }

    # try to find the key holding the subobject or a list of subobjects
    for k, v in new.items():
        # list of subobjects
        if isinstance(v, list):
            del new[k]
            return new, k, v
        # or just one subobject
        elif isinstance(v, dict):
            del new[k]
            return new, k, [v]
    return new, None, None

def flatten (data, prefix = None):
    '''
    Flatten the data, optionally with each key prefixed.
    '''
    # iterate all items
    for item in data:
        # split the object
        flat, key, subs = splitObj(item, prefix)

        # just return fully flat objects
        if key is None:
            yield flat
            continue

        # otherwise recursively flatten the subobjects
        for sub in flatten(subs, key):
            sub.update(flat)
            yield sub

我收到以下错误:

AttributeError: 'str' object has no attribute 'items'

来自'states': ['USED']的结果

我不知道如何处理。关键的“状态”可以保存为一个列表。

我希望有人可以帮助我。

Ps:这是Python: Write Nested JSON as multiple elements in List的后续帖子

【问题讨论】:

    标签: python json list dictionary flatten


    【解决方案1】:

    这是我的 splitObj 解决方案

    def splitObj (obj, prefix = None):
    '''
    Split the object, returning a 3-tuple with the flat object, optionally
    followed by the key for the subobjects and a list of those subobjects.
    obj needs to be a Dictonary
    '''
    # copy the object, optionally add the prefix before each key
    new = obj.copy() if prefix is None or prefix=="NotFlat" else { '{}_{}'.format(prefix, k): v for k, v in obj.items() }
    
    cL = 0
    cD = 0
    # try to find the key holding the subobject or a list of subobjects
    for k, v in new.items():
        #Determine the number of lists in v
        if isinstance(v, list):
            cL += 1
        #Determine the number of dict in v
        elif isinstance(v, dict):
            cD += 1     
    for k, v in new.items():
        # list of subobjects
        if isinstance(v, list):
            if (cD+cL) <=1:
                try:
                    type(v[0])
                except IndexError:
                    v = [""]
                if not isinstance(v[0], str):
                    del new[k]
                    return new, k, v
                elif isinstance(v[0], str):
                    #handle list when only containing strings, return, the whole thing
                    #solve other dicts which might be in the line
                    #use "NotFlat" to run loop again but without adding a prefix
    
                    new[k] = ", ".join(v)
                    return new, None, None
                else:
                    custLog.logger.info("")
            elif (cD+cL) >1:
    
                #print("Count List2 CD: "+str(cD))
                #print("Count LIST2 CL: "+str(cL))
    
                #if list is empty
                try:
                    type(v[0])
                except IndexError:
                    v = [""]
    
                if not isinstance(v[0], str):
                    del new[k]
                    for x in flatten([new]):
                        newOut = x
                        break
                    return newOut, k, v
                elif isinstance(v[0], str):
                    #handle list when only containing strings, return, the whole thing
                    #solve other dicts which might be in the line
                    #use "NotFlat" to run loop again but without adding a prefix
                    new[k] = ", ".join(v)
                    return None, "NotFlat", [new]
                else:
                    custLog.logger.error("weder noch 2")
    
        # or just one subobject
        elif isinstance(v, dict):
            if (cD+cL) <=1:
                del new[k]
                return new, k, [v]
            elif (cD+cL) >1:
                del new[k]
                for x in flatten([new]):
                    newOut = x
                    break
                return newOut, k, [v]
    return new, None, None
    

    这里是扁平化

    def flatten (data, prefix = None):
        '''
        Flatten the data, optionally with each key prefixed.
        '''
        # iterate all items
    
    
        for item in data:
            # split the object
            flat, key, subs = splitObj(item, prefix)
            if subs is None:
                if key is None:
                    yield flat
                    continue    
            # just return fully flat objects
            if key is None and flat is not None:
                yield flat
                continue
    
            # otherwise recursively flatten the subobjects
            try:
                for sub in flatten(subs, key):
                    if flat is not None:
                        sub.update(flat)
                    yield sub
            except TypeError as e:
                custLog.logger.error("ERR: TypeError"+str(e))
    

    【讨论】:

      【解决方案2】:

      虽然不是通用函数,但请考虑遍历每个嵌套元素以获得用于数据库导入或平面文件(csv、txt)导出的平面输出。由于 json 文件由字典和列表的组合组成,因此在每个级别进行相应的处理:

      items = []
      for outer in data:    
          inner = [''] * 15    
          for outerk, outerv in outer.items():        
              inner[0] = outer['states'][0]
              inner[1] = outer['niceName']
              inner[2] = outer['id']
              inner[3] = outer['make']['niceName']
              inner[4] = outer['make']['name']
              inner[5] = outer['make']['id']    
              if outerk == 'years':            
                  for yri in outer[outerk]:                
                      for yrk, yrv in yri.items():
                          inner[6] = yri['states'][0] 
                          inner[7] = yri['id'] 
                          inner[8] = yri['year'] 
                          if yrk == 'styles':
                              for stylei in yri[yrk]:
                                  inner[9] = stylei['trim']
                                  inner[10] = stylei['name']
                                  inner[11] = stylei['id']
                                  inner[12] = stylei['submodel']['body']
                                  inner[13] = stylei['submodel']['niceName']
                                  inner[14] = stylei['submodel']['modelName']
      
                                  items.append(inner[0:14])
      
      for i in items:        
          print(i)
      

      输出 (父项为每个子项重复)

      # ['USED', '1-series', 'BMW_1_Series', 'bmw', 'BMW', 200000081, 'USED', 100524709, 2008, '128i', '128i 2dr Convertible (3.0L 6cyl 6M)', 100994560, 'Convertible', 'convertible']
      # ['USED', '1-series', 'BMW_1_Series', 'bmw', 'BMW', 200000081, 'USED', 100524709, 2008, '128i', '128i 2dr Coupe (3.0L 6cyl 6M)', 100974974, 'Coupe', 'coupe']
      # ['USED', '1-series', 'BMW_1_Series', 'bmw', 'BMW', 200000081, 'USED', 100524709, 2008, '135i', '135i 2dr Coupe (3.0L 6cyl Turbo 6M)', 100974975, 'Coupe', 'coupe']
      # ['USED', '1-series', 'BMW_1_Series', 'bmw', 'BMW', 200000081, 'USED', 100524709, 2008, '135i', '135i 2dr Convertible (3.0L 6cyl Turbo 6M)', 100994561, 'Convertible', 'convertible']
      # ['USED', '1-series', 'BMW_1_Series', 'bmw', 'BMW', 200000081, 'USED', 100503222, 2009, '135i', '135i 2dr Coupe (3.0L 6cyl Turbo 6M)', 101082656, 'Coupe', 'coupe']
      

      【讨论】:

        【解决方案3】:

        重新思考问题

        通常更容易为更普遍的问题找到解决方案。所以,让我们先仔细看看这个问题。

        输入是一些描述一组对象的 JSON 文件。

        对象被递归定义为原子(字符串或数字)或具有对象值的字典。列表用于表示备选方案(即列表中的任何元素都可以代替列表)。 例如{a:[1,2]} 表示a 可以是12

        输出应该是不包含任何选项的对象列表。此外,对象应该是扁平的,即应该是 dicts,其值为原子,其键描述原始对象中值的路径。

        我的解决方案分别处理替代方案和展平。

        标准化

        下面的函数normalise 接受json.dumps 的输入并产生一个dicts 序列。请注意,normalise 的输入和输出具有相同的语义,并且描述了相同的对象集。输出只是归一化,因为它确实只在顶层包含替代方案。数据库人员会将其称为非规范化,因为它不适合关系数据库。

        normalise 总是返回一个对象序列。 normalise 被实现为生成器以保持低内存使用率。

        normalise区分以下情况。

        • 原子输入意味着只有一种可能性。因此,产生了原子(这就像返回一个包含原子的列表)。
        • 列表表示备选方案的备选方案。它产生其规范化输入的所有元素(这就像连接列表)。
        • 字典意味着我们必须考虑各个键的所有备选组合。因此,我们返回备选方案的笛卡尔积。

        代码如下:

        import itertools
        
        def normalise(x):
            if isinstance(x, dict):
                keys = x.keys()
                values = (normalise(i) for i in x.values())
                for i in itertools.product(*values):
                    yield (dict(zip(keys, i)))
            elif isinstance(x, list):
                #if not x:           # uncomment for "LEFT JOIN" behaviour
                #    yield None
                for i in x:
                    yield from normalise(i)
            else:
                yield x
        

        如果该代码包含任何空列表,则此代码不会返回对象。这是因为没有可能的价值。这就像 SQL “INNER JOIN”。从 Bert 的回答看来,他想要“LEFT JOIN”行为(即一些默认值)。要实现这一点,只需取消注释这两行。

        伪扁平化

        normalise 产生的对象仍然具有原始(嵌套)dict 结构。可以使用其他讨论中的代码将它们展平。

        但是,OP 想要在数据库中插入对象。因此,他很可能不需要扁平化字典的键列表。他只需要一个返回给定路径值的函数。

        这可以通过为具有__getitem__ 方法的dict 创建一个包装器对象来实现。此包装器还可用于为不存在的路径返回默认值。

        class DictWrapper:
            def __init__(self, d, sep='.'):
                self.d = d
                self.sep = sep
        
            def __getitem__(self, key):
                ret = self.d
                try:
                    for k in key.split(self.sep):
                        ret = ret[k]
                    return ret
                except KeyError:
                    return None
        

        sql 插入可以如下所示(用 psycopg2 测试)

        for i in normalise(input):
            cur.execute('insert into mytable (year) VALUES (%(years.year)s)', DictWrapper(i))
        

        实现细节

        • 为了清晰起见,这种实现显然牺牲了一些运行时性能。

        • 可以使用抽象基类代替listdict。但是,这可能会有问题,因为str 是一个序列,但应该被视为原子。

        • DictWrapper 仅在 sep 不包含在任何 dict 键中时才能正常工作。

        • normalise 不会过滤掉重复项。这可以通过使用集合和命名元组而不是列表和字典来完成。但是,这意味着整个结果必须在内存中。最好在数据库级别过滤掉重复项。

        • 为了将内存使用量保持在最低限度,应延迟读取 JSON。

        【讨论】:

          猜你喜欢
          • 2022-11-10
          • 2016-08-11
          • 1970-01-01
          • 2016-11-14
          • 1970-01-01
          • 2023-01-19
          • 2020-03-15
          • 1970-01-01
          • 2018-12-11
          相关资源
          最近更新 更多