Formaty danych które powinien znać każdy programista

Format danych to sposób w jaki zapisujemy dane – mamy tutaj spory wybór i to od niego zależy dość mocno wygoda jaką będą miały osoby korzystające z udostępnionych przez nas danych. W ramach tego artykułu przejdziemy sobie przez kilka najpopularniejszych – czyli takich które każdy programista powinien znać :)

Wpis jest efektem szkolenia które przeprowadziłem 26ego września w Krakowie, gdzie w ramach całego, dużego kursu Pythona, opowiadałem o sposobach korzystania z różnego rodzaju API jak i o dobrych praktykach przy jego tworzeniu. Scenariusz zajęć stworzyłem tak by był pełen zadań praktycznych które mocno dotykały właśnie formatów danych, co podczas zajęć okazało się niemniej ważne niż HTTP czy REST. Udało się oczywiście wszystko fajnie przedstawić ale zrodziło to właśnie pomysł na ten wpis :)

Przykłady zapisu i odczytu różnych formatów będą w Pythonie ale bez problemu znajdziesz w Internecie przykłady jak zrobić to w innych językach.

No ale żeby nie przedłużać, zacznijmy od chyba najpopularniejszego w całym programowaniu formatu danych – od XMLa.

XML

XML to język znaczników przeznaczony do reprezentowania różnych danych w strukturalizowany sposób. Jest niezależny od platformy, co umożliwia łatwą wymianę dokumentów pomiędzy różnymi systemami. Jest używany w bardzo popularnych protokole wymiany informacji – SOAP.

Do parsowania tego formatu danych używa się dwóch podejść:

  • SAX – w tym podejściu patrzymy na plik XML jak na strumień danych i definiujemy odpowiednie metody które parser będzie uruchamiał w momencie wystąpienia jakiegoś zdarzenia np. gdy wejście w jakiś element, to nasza metoda może wydobyć jakiś atrybut z niego.
  • DOM – patrzymy na plik XML jak na model obiektowy. Parsery tego typu dostarczają nam metody w stylu „getElementsByTagName” – bez potrzeby obsługi, jak w SAXie, samego procesu przetwarzania.

Oba podejścia mają swoje wady i zalety i np. model DOM jest oczywiście prostszy w używaniu ale wymaga załadowania całego pliku do pamięci – co może okazać się śliskie. SAX nie trzyma w pamięci zbyt dużo – on uruchamia zdefiniowane metody w momencie czytania samego pliku – więc do pamięci trafiają już tylko wyłuskane przez nas dane. Patrząc z innej strony – SAX jest jednokierunkowy, tzn jeśli odwiedzimy jakiś znacznik to do poprzedniego już nie wrócimy. Przy modelu DOM – możemy chodzić po stworzonym drzewie w każdą stronę.

Więc który lepszy? Ano odpowiedź jest taka jak ulubiona odpowiedź każdego seniora/lidera/experta – i brzmi „to zależy do kontekstu” :) więc wybierając sposób odczytu – ustal pierw jak duże te pliki będą, ile masz pamięci i jak szybko ma to działać.

Przykład

Przykład zamiany słownika na xml i na odwrót:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from xml.dom.minidom import parseString
from dicttoxml import dicttoxml

data = {
  'blog_url': 'https://mmazurek.dev',
  'blog_rate': 10,
  'blog_max_rate': 10,
  'blog_keywords': ['programowanie', 'python', 'proces', 'tworzenia', 'programowania']
}
dict_as_xml = dicttoxml(data, custom_root='blog')

"""
<?xml version="1.0" ?>
<blog>
  <blog_url type="str">https://mmazurek.dev</blog_url>
  <blog_rate type="int">10</blog_rate>
  <blog_max_rate type="int">10</blog_max_rate>
  <blog_keywords type="list">
      <item type="str">programowanie</item>
      <item type="str">python</item>
      <item type="str">proces</item>
      <item type="str">tworzenia</item>
      <item type="str">programowania</item>
  </blog_keywords>
</blog>

"""


xml = parseString(dict_as_xml)

xml_as_dict = {}

blog = xml.firstChild

for item in blog.childNodes[0:-1]:
  xml_as_dict[item.tagName] = item.firstChild.nodeValue

last_key = blog.childNodes[-1].tagName
xml_as_dict[last_key] = []

for item in blog.childNodes[-1].childNodes:
  xml_as_dict[last_key].append(item.firstChild.nodeValue)

print(xml_as_dict)

"""
{'blog_url': 'https://mmazurek.dev', 'blog_rate': '10', 'blog_max_rate': '10', 'blog_keywords': ['programowanie', 'python', 'proces', 'tworzenia', 'programowania']}
"""

Do zamiany słownika na xml użyłem biblioteki dicttoxml. Należy ją doinstalować pip’em. Xmla zamieniłem na słownik używając prostej implementacji DOM o nazwie minidom.

JSON

Jestem prawie pewien że każdy kto czyta ten artykuł zna JSONa – to obecnie najpopularniejszy format danych. Używany jest w wywołaniach AJAX np. w REST, chociaż żadna specyfikacja tego nie nakazuje.

Podrzucam gramatykę JSONa: https://github.com/antlr/grammars-v4/blob/master/json/JSON.g4 czyli po prostu formalny opis składni.

Przykład

1
2
3
4
5
6
7
8
9
10
11
12
from json import loads, dumps

data = {
  'blog_url': 'https://mmazurek.dev',
  'blog_rate': 10,
  'blog_max_rate': 10,
  'blog_keywords': ['programowanie', 'python', 'proces', 'tworzenia', 'programowania']
}

dict_as_json = dumps(data)

json_as_dict = loads(dict_as_json)

W Pythonie mamy w standardowej bibliotece moduł json który posiada metody loads/dumps oraz ich odpowiedniki dla pracy na plikach: load/dump.

Wspomnę jeszcze o problemie serializacji własnych obiektów:

1
2
3
4
5
6
7
8
9
10
11
12
13
from json import loads, dumps


class Blog:
  def __init__(self):
      self.property = "value"


obj_as_json = dumps(Blog())

"""
TypeError: Object of type Blog is not JSON serializable
"""

Próba serializacji własnych obiektów skutkuje wyjątkiem TypeError, w prostych przypadkach można użyć do serializacji słownikowej reprezentacji instancji obiektu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from json import loads, dumps


class Blog:
  def __init__(self):
      self.property = "value"


obj_as_json = dumps(Blog().__dict__)

print(obj_as_json)
"""
{"property": "value"}
"""

YAML

Jak pierwszy raz zobaczyłem YAMLa (YAML Ain’t Markup Language) to od razu przypominał mi Pythona – tutaj zakres danych i relacje rodzic-dziecko realizowana jest za pomocą wcięć. Znalazł się w tym artykule ponieważ zyskuje popularność :)

Używany jest np. w Ansiblu czy Kubernetesie.

Przykład

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from yaml import safe_load, safe_dump

data = {
  'blog_details': {
      'blog_url': 'https://mmazurek.dev',
      'blog_rate': 10,
      'blog_max_rate': 10,
      'blog_keywords': ['programowanie', 'python', 'proces', 'tworzenia', 'programowania'],
      'additional_info': [None, True, {
          "type": ['intresting', 'type']
      }]
  }
}
dict_as_yml = safe_dump(data)
print(dict_as_yml)

Efekt tego programu to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
blog_details:
additional_info:
- null
- true
- type:
  - intresting
  - type
blog_keywords:
- programowanie
- python
- proces
- tworzenia
- programowania
blog_max_rate: 10
blog_rate: 10
blog_url: https://mmazurek.dev

co jest dość ciekawe, bo o ile założenie że JSONa zna każdy jest bezpieczne, to za YAML’a nie dałbym niczego sobie uciąć.

Tak jak napisałem wyżej – relacje rodzic-dziecko realizowane są wcięciami a listy znakami „-„. W przeciwieństwie do JSONa, YAML umożliwia odwołanie się do innych elementów samego siebie – co pozwala na to by nie duplikować danych.

Do obsługi tego formatu należy doinstalować moduł pyyaml.

INI

Ten format danych możesz spotkać najczęściej jako sposób trzymania konfiguracji. Mamy tu bardzo proste relacje – tworzymy sekcje a pod sekcją może być para klucz-wartość. INI nie pozwala (na tyle na ile wiem, jeśli się mylę – popraw mnie w komentarzu) na definiowanie list.

Przykład

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from configparser import ConfigParser

data = {
  'blog_url': 'https://mmazurek.dev',
  'blog_rate': 10,
  'blog_max_rate': 10,
  'blog_keywords': ['programowanie', 'python', 'proces', 'tworzenia', 'programowania']
}

config = ConfigParser()
config.add_section('blog')

for key, val in data.items():
  if not isinstance(val, list):
      config.set('blog', key, str(val))
  else:
      config.add_section(key)
      config.set(key, key, ','.join(val))

with open("inifile.ini", "w") as f:
  config.write(f)

Co wygeneruje taki plik:

[blog]
blog_url = https://mmazurek.dev
blog_rate = 10
blog_max_rate = 10

[blog_keywords]
blog_keywords = programowanie,python,proces,tworzenia,programowania

Zauważ że listę zrealizowaliśmy „na piechotę”. Można ten format połączyć z JSONem i zrobić coś takiego:

[blog]
blog_url = https://mmazurek.dev
blog_rate = 10
blog_max_rate = 10
blog_keywords = ['programowanie', 'python', 'proces', 'tworzenia', 'programowania']

Dane binarne

Te formaty które pokazałem wyżej to formaty tekstowe. Poza formatem tekstowym, czyli takim który gołym okiem może zostać odczytany, jest jeszcze format binarny w którym obiekt zapisujemy binarnie. Ma to swoje wady – jak np. to że dane takie nie są możliwe do przeniesienia pomiędzy językami, ba, czasem nawet pomiędzy programami w tym samym języku jest problem. Ma też zalety – zapisując binarnie nie martwimy się o to ze ktoś nam dane w takim formacie zmieni – nawet jeśli zmieni to po zmianie nie będą nadawać się do deserializacji.

W Pythonie mamy moduł pickle i używa się go tak:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pickle import load, dump

data = {
  'blog_url': 'https://mmazurek.dev',
  'blog_rate': 10,
  'blog_max_rate': 10,
  'blog_keywords': ['programowanie', 'python', 'proces', 'tworzenia', 'programowania']
}


with open('data.pickle', 'wb') as f:
  dump(data, f)

with open('data.pickle', 'rb') as f:
  data = load(f)

print(data)

"""
{'blog_url': 'https://mmazurek.dev', 'blog_rate': 10, 'blog_max_rate': 10, 'blog_keywords': ['programowanie', 'python', 'proces', 'tworzenia', 'programowania']}
"""

CSV

Jak pisałem ten artykuł to całkowicie uciekł mi ten format danych. Ale za to czytelnicy przypomnieli, więc uzupełniam.

CVS to Comma Separated Values. Prosty typ danych gdzie wartości oddzielamy przecinkiem a kolejne wiersze – znakiem nowej linii. Przykładowo może on wyglądać tak:

Kowalski,Jan,Kłodzko 
Nowak,Zenon,Szczecin Brzęczyszczykiewicz,Grzegorz,Chrząszczyżewoszyce

W Pythonie mamy małego liba do pracy z csv:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from csv import reader
from io import StringIO
from json import dumps


csv_data = """Kowalski,Jan,Kłodzko
Nowak,Zenon,Szczecin
Brzęczyszczykiewicz,Grzegorz,Chrząszczyżewoszyce"""


csv_reader = reader(StringIO(csv_data), delimiter=',')

csv_as_list = list(csv_reader)

print(dumps(csv_as_list))

którym możemy łatwo czytać dane w tym formacie a co za tym idzie – konwertować na inne formaty danych.

Zmierzając do brzegu

Mimo że JSON jest najpopularniejszym formatem danych to nigdy nie wiadomo do końca z czym będziemy mieć przyjemność pracować. Może ta integracja z jakimś nowym API co czeka na przyszły sprint, okazać się świetną sposobnością do tego by przekonać się o pomysłowości programistów.

A jeśli nie, to przecież wiedzy nigdy za dużo :)

Dzięki za wizytę,
Mateusz Mazurek
Mateusz M.

Pokaż komentarze

  • >W przeciwieństwie do JSONa, YAML umożliwia do innych elementów samego siebie – co pozwala na to by nie duplikować danych.

    Nie rozumiem

    • Fakt.. Poprawiłem :P chodziło o to ze w YAMLu możesz zrobić referencję do innego elementu tego dokumentu:)

  • Przy dużych ilościach danych o stałej i prostej strukturze przydaje się stary, dobry csv :)

  • Nie wiedziałem, że jsona można konwertować do yamla i... ini?!? Czy można konwertować ini z powrotem na json'? Albo nawet idąc dalej zdeserializować go na obiekt? To by znacznie ułatwiło sprawę zrobienia prostych plików konfiguracyjnych.
    Przy ML dużo pracowałem z CSV, a przy budowie sklepów konieczne było przechowywanie blob'ów - może przyda się na następny artykuł.
    Ps. Jest output :)

  • Yaml i ini to formaty danych z którymi jeszcze nie miałem styczności, natomiast z JSON oraz XML trochę już programowałem. Sam nie wiem który z tych formatów jest lepszy. Chyba z JSON mi się lepiej działało..

    Dzięki za fajny wpis na twoim blogu Mateusz.

  • Wpis wędruje do ulubionych jako mała ściągawka. Dzięki! Ciężko się z tym wszystkim połapać na początku nauki programowania..

    • Cieszę się że mogłem sprawić że Twoje życie jest łatwiejsze :D

Ostatnie wpisy

Podsumowanie: maj, czerwiec, lipiec i sierpień 2024

Oj daaawnoo mnie tu nie było. Ale wakacje to był czas dużej liczby intensywnych wyjazdów i tak naprawdę, dopiero jakoś… Read More

2 miesiące ago

Podsumowanie: kwiecień 2024

Cześć! Zapraszam na krótkie podsumowanie kwietnia. Wyjazd do Niemiec A dokładniej pod granicę z Francją. Chrześnica miała pierwszą komunię. Po… Read More

6 miesięcy ago

Podsumowanie: luty i marzec 2024

Ostatnio tygodnie były tak bardzo wypełnione, że nie udało mi się napisać nawet krótkiego podsumowanie. Więc dziś zbiorczo podsumuję luty… Read More

7 miesięcy ago

Podsumowanie: styczeń 2024

Zapraszam na krótkie podsumowanie miesiąca. Książki W styczniu przeczytałem "Homo Deus: Historia jutra". Książka łudząco podoba do wcześniejszej książki tego… Read More

9 miesięcy ago

Podsumowanie roku 2023

Cześć! Zapraszam na podsumowanie roku 2023. Książki Zacznijmy od książek. W tym roku cel 35 książek nie został osiągnięty. Niemniej… Read More

10 miesięcy ago

Podsumowanie: grudzień 2023

Zapraszam na krótkie podsumowanie miesiąca. Książki W grudniu skończyłem czytać Mein Kampf. Nudna książka. Ciekawsze fragmenty można by było streścić… Read More

10 miesięcy ago