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 :)
Mateusz Mazurek
>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 :)
Potwierdzam! W ogóle zostanie dopisany tu:P
Mam nadzieje ze w końcu yaml wyprze ten nieczytelny json ;)
Myślę że chwilę to może potrwać:P
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