Как преобразовать несколько источников данных в предопределенную структуру данных в python?

#python #json #serialization

Вопрос:

Я создаю анализатор, который берет данные из нескольких источников с несколькими схемами данных, а затем преобразует их в стандартизированную структурированную схему.

Например, у меня есть 2 источника данных:

Источник 1:

 {
  "students: [{
     "id": 129939,
     "name": "Alice",
     "gender": "female",
  }]
}
 

Источник 2:

 {
  "students: [{
     "id": 129939,
     "fullname": "Alice",
     "sex": "female",
  }]
}
 

Оба источника данных могут быть преобразованы в стандартизированные структурированные данные, которые я уже определил:

 class Student:
   id: int
   name: str
   gender: str
 

Знаете ли вы, существует ли какая-либо существующая библиотека, которая поддерживает определение схемы для каждого источника входных данных, а затем позволяет сопоставлять каждое поле источника входных данных с требуемой структурой данных?

Например, это может быть картограф, подобный этому:

 class Input1toStudentMapper:
   id -> Student.id
   name -> Student.name
   gender -> Student.gender
 
 class Input2toStudentMapper:
   id -> Student.id
   fullname -> Student.name
   sex -> Student.gender
 

Любое предложение будет оценено по достоинству.

Комментарии:

1. Почему бы вам не выбрать одну структуру и не создать для нее синтаксический анализатор, а затем вы можете использовать регулярное выражение для редактирования jsons. например, прочитайте json, используйте re.sub, чтобы заменить «полное имя» на «имя», а затем проанализируйте

Ответ №1:

Я не знаю ни одной библиотеки, но для проблемы, которую вы описываете, вы, вероятно, можете закодировать логику в __init__ самом классе —

 class Student:
    name_field_variations = ['name', 'fullname']
    sex_field_variations = ['gender', 'sex']
    def __init__(self, **kwargs):
        self.id = kwargs['id']
        _name_field = set(kwargs.keys()) amp; set(Student.name_field_variations)
        self.name = kwargs[_name_field.pop()]
        _sex_field = set(kwargs.keys()) amp; set(Student.sex_field_variations)
        self.gender = kwargs[_sex_field.pop()]

print(js1) # {'students': [{'id': 129939, 'name': 'Alice', 'gender': 'female'}]}
print(js2) # {'students': [{'id': 129939, 'fullname': 'Alice', 'sex': 'female'}]}
s1 = Student(**js1['students'][0])
s2 = Student(**js2['students'][0])
print(s1.gender) # female
print(s2.gender) # female
 

Ответ №2:

Я бы проверил библиотеку мастера классов данных для этого. Он хорошо работает со встроенным модулем dataclasses в Python. Он поддерживает несколько псевдонимов (или сопоставлений ключей) для каждого поля, а также односторонний псевдоним, который нам может понадобиться в этом случае-например, если мы хотим разрешить дополнительное сопоставление fullname с name полем, но сериализовать с использованием ключа по умолчанию name .

В приведенном ниже примере я также добавил __future__ импорт, который поддерживается в Python 3.7 или выше. В основном это делается для поддержки использования прямых ссылок; без этого будущего импорта нам потребуется явно определить прямые ссылки, например .как List['Student'] .

 from __future__ import annotations

from dataclasses import dataclass
from typing import List

from dataclass_wizard import json_key
from typing_extensions import Annotated


@dataclass
class Container:
    students: List[Student]


@dataclass
class Student:
    id: int
    name: Annotated[str, json_key('fullname')]
    gender: Annotated[str, json_key('sex')]
 

В приведенной выше аннотации объявление его как json_key('fullname') является сокращением json_key('name', 'fullname', all=True) , что также позволяет сопоставлять несколько псевдонимов с полем.

Обратите внимание, что если вы планируете поддерживать только Python 3.9 или выше, вы можете внести следующие изменения:

  • Вместо этого импортируйте Annotated из typing модуля
  • Удалите from typing import List импорт и определите аннотацию следующим образом list[Student]

Вот пример использования для тестирования приведенного выше кода:

 if __name__ == '__main__':
    from dataclass_wizard import asdict, fromdict, fromlist

    source_1 = {
        "students": [{
            "id": 129939,
            "name": "Alice",
            "gender": "female",
        }]
    }

    source_2 = {
        "students": [{
            "id": 129940,
            "fullname": "Johnny",
            "sex": "male",
        }]
    }

    c1 = fromdict(Container, source_1)
    print(c1)
    # Container(students=[Student(id=129939, name='Alice', gender='female')])

    c2 = fromdict(Container, source_2)
    print(c2)
    # Container(students=[Student(id=129940, name='Johnny', gender='male')])

    # alternatively, if you just need a list of the `Student` instances:
    students = fromlist(Student, source_1['students'])
    print(students)
    # [Student(id=129939, name='Alice', gender='female')]

    # assert we get the same data when serializing the Container instance as a
    # Python dict object.
    serialized_dict = asdict(c1)
    assert serialized_dict == source_1