Intenté buscar preguntas similares, pero ninguna de las que encontré hace lo que necesito.

Estoy tratando de construir una función genérica que pueda tomar los 2 argumentos:

  1. La estructura del objeto
  2. Una lista de rutas (anidadas)

Y convierta todas las rutas dadas en una lista de diccionarios planos, adecuados para la salida en formato CSV.

Entonces, por ejemplo, si tengo una estructura como:

structure = {
    "configs": [
        {
            "name": "config_name",
            "id": 1,
            "parameters": [
                {
                    "name": "param name",
                    "description": "my description",
                    "type": "mytype",
                },
                {
                    "name": "test",
                    "description": "description 2",
                    "type": "myothertype",
                    "somedata": [
                        'data',
                        'data2'
                    ]
                }
            ]
        },
        {
            "name": "config_name2",
            "id": 2,
            "parameters": [
                {
                    "name": "param name",
                    "description": "my description",
                    "type": "mytype2",
                    "somedata": [
                        'data',
                        'data2'
                    ]
                },
                {
                    "name": "test",
                    "description": "description 2",
                    "type": "myothertype2",
                }
            ]
        }
    ]
}

Y pase la siguiente lista de rutas:

paths = [
'configs.name', # notice the list structure is omitted (i.e it should be 'configs.XXX.name' where XXX is the elem id). This means I want the name entry of every dict that is in the list of configs
'configs.0.id', # similar to the above but this time I want the ID only from the first config
'configs.parameters.type' # I want the type entry of every parameter of every config
]

A partir de esto, la función debería generar una lista de diccionarios planos. Cada entrada de la lista corresponde a una única fila del CSV. Cada diccionario plano contiene todas las rutas seleccionadas.

Entonces, por ejemplo, en este caso debería ver:

result = [
{"configs.name": "config_name", "configs.0.id": 1, "configs.parameters.type": "mytype"},
{"configs.name": "config_name", "configs.0.id": 1, "configs.parameters.type": "myothertype"},
{"configs.name": "config_name2", "configs.parameters.type": "mytype2"},
{"configs.name": "config_name2", "configs.parameters.type": "myothertype2"}
]

Necesita poder hacer esto para cualquier estructura pasada, que contenga dictados y listas anidados.

EDITAR:

Probé el código de @ Ajax1234 y parece que hay un error en él; en algunos casos, obtiene el doble de elementos de lo esperado. El error se demuestra en el siguiente código:

SOLUCIONADO : Problema resuelto por @ Ajax1234 editar

import pprint


def get_val(d, rule, match = None, l_matches = []):
   if not rule:
      yield (l_matches, d)
   elif isinstance(d, list):
     if rule[0].isdigit() and (match is None or match[0] == int(rule[0])):
        yield from get_val(d[int(rule[0])], rule[1:], match=match if match is None else match[1:], l_matches=l_matches+[int(rule[0])])
     elif match is None or not rule[0].isdigit():
         for i, a in enumerate(d):
            if not match or i == match[0]:
               yield from get_val(a, rule, match=match if match is None else match[1:], l_matches = l_matches+[i])
   else:
      yield from get_val(d[rule[0]], rule[1:], match = match, l_matches = l_matches)

def evaluate(paths, struct, val = {}, rule = None):
   if not paths:
      yield val
   else:
      k = list(get_val(struct, paths[0].split('.'), match = rule))
      if k:
         for a, b in k:
            yield from evaluate(paths[1:], struct, val={**val, paths[0]:b}, rule = a)
      else:
         yield from evaluate(paths[1:], struct, val=val, rule=rule)
         
paths1 = ['configs.id', 'configs.parameters.name', 'configs.parameters.int-param'] # works as expected
paths2 = ['configs.parameters.name', 'configs.id', 'configs.parameters.int-param'] # prints everything twice

structure = {
    'configs': [
        {
            'id': 1,
            'name': 'declaration',
            'parameters': [
                {
                    'int-param': 0,
                    'description': 'decription1',
                    'name': 'name1',
                    'type': 'mytype1'
                },
                {
                    'int-param': 1,
                    'description': 'description2',
                    'list-param': ['param0'],
                    'name': 'name2',
                    'type': 'mytype2'
                }
            ]
        }
    ]
}

pprint.PrettyPrinter(2).pprint(list(evaluate(paths2, structure)))

La salida que usa la lista paths1 es:

[ { 'configs.id': 1,
    'configs.parameters.int-param': 0,
    'configs.parameters.name': 'name1'},
  { 'configs.id': 1,
    'configs.parameters.int-param': 1,
    'configs.parameters.name': 'name2'}]

Mientras que la salida de paths2 produce:

[ { 'configs.id': 1,
    'configs.parameters.int-param': 0,
    'configs.parameters.name': 'name1'},
  { 'configs.id': 1,
    'configs.parameters.int-param': 1,
    'configs.parameters.name': 'name1'},
  { 'configs.id': 1,
    'configs.parameters.int-param': 0,
    'configs.parameters.name': 'name2'},
  { 'configs.id': 1,
    'configs.parameters.int-param': 1,
    'configs.parameters.name': 'name2'}]
1
Slav 22 ene. 2021 a las 15:16

1 respuesta

La mejor respuesta

Puede crear una función de búsqueda que busque valores según sus reglas (get_val). Además, esta función toma una lista de coincidencias (match) de índices válidos que le dice a la función que solo recorra las sublistas del diccionario que tengan un índice coincidente. De esta manera, la función de búsqueda puede "aprender" de búsquedas anteriores y solo devolver valores basados ​​en el posicionamiento de la sublista de búsquedas anteriores:

structure = {'configs': [{'name': 'config_name', 'id': 1, 'parameters': [{'name': 'param name', 'description': 'my description', 'type': 'mytype'}, {'name': 'test', 'description': 'description 2', 'type': 'myothertype', 'somedata': ['data', 'data2']}]}, {'name': 'config_name2', 'id': 2, 'parameters': [{'name': 'param name', 'description': 'my description', 'type': 'mytype2', 'somedata': ['data', 'data2']}, {'name': 'test', 'description': 'description 2', 'type': 'myothertype2'}]}]}
def get_val(d, rule, match = None, l_matches = []):
   if not rule:
      yield (l_matches, d)
   elif isinstance(d, list):
     if rule[0].isdigit() and (match is None or match[0] == int(rule[0])):
        yield from get_val(d[int(rule[0])], rule[1:], match=match if match is None else match[1:], l_matches=l_matches+[int(rule[0])])
     elif match is None or not rule[0].isdigit():
         for i, a in enumerate(d):
            if not match or i == match[0]:
               yield from get_val(a, rule, match=match if match is None else match[1:], l_matches = l_matches+[i])
   else:
      yield from get_val(d[rule[0]], rule[1:], match = match, l_matches = l_matches)

def evaluate(paths, struct, val = {}, rule = None):
   if not paths:
      yield val
   else:
      k = list(get_val(struct, paths[0].split('.'), match = rule))
      if k:
         for a, b in k:
            yield from evaluate(paths[1:], struct, val={**val, paths[0]:b}, rule = a)
      else:
         yield from evaluate(paths[1:], struct, val=val, rule = rule)

paths = ['configs.name', 'configs.0.id', 'configs.parameters.type']
print(list(evaluate(paths, structure)))

Salida:

[{'configs.name': 'config_name', 'configs.0.id': 1, 'configs.parameters.type': 'mytype'}, 
 {'configs.name': 'config_name', 'configs.0.id': 1, 'configs.parameters.type': 'myothertype'}, 
 {'configs.name': 'config_name2', 'configs.parameters.type': 'mytype2'}, 
 {'configs.name': 'config_name2', 'configs.parameters.type': 'myothertype2'}]

Editar: es mejor ordenar sus rutas de entrada por profundidad de ruta en el árbol:

def get_depth(d, path, c = 0):
   if not path:
      yield c
   elif isinstance(d, dict) or path[0].isdigit():
      yield from get_depth(d[path[0] if isinstance(d, dict) else int(path[0])], path[1:], c+1)
   else:
      yield from [i for b in d for i in get_depth(b, path, c)]

Esta función encontrará la profundidad en el árbol a la que existe el valor objetivo de la ruta. Luego, para aplicar al código principal:

structure = {'configs': [{'id': 1, 'name': 'declaration', 'parameters': [{'int-param': 0, 'description': 'decription1', 'name': 'name1', 'type': 'mytype1'}, {'int-param': 1, 'description': 'description2', 'list-param': ['param0'], 'name': 'name2', 'type': 'mytype2'}]}]}
paths1 = ['configs.id', 'configs.parameters.name', 'configs.parameters.int-param']
paths2 = ['configs.parameters.name', 'configs.id', 'configs.parameters.int-param']
print(list(evaluate(sorted(paths1, key=lambda x:max(get_depth(structure, x.split('.')))), structure)))
print(list(evaluate(sorted(paths2, key=lambda x:max(get_depth(structure, x.split('.')))), structure)))
1
Ajax1234 23 ene. 2021 a las 16:48