https://pixabay.com/pl/informacji-dane-dysk-serwer-1641937/

Python i (de)XML

python.org

W ostatnich dwóch tygodniach dwukrotnie uczestniczyłem w rozmowie na temat XML, co skłoniło mnie do sprawdzenia, jak to wygląda w języku Python. Skutkiem tego jest krótki przerywnik od projektu na DSP2017 w postaci tego wpisu.

Nie chcę tu opisywać podstaw, czym jest XML, do czego się go stosuje. Nie chcę również pisać o DOM czy SAX. Założenie było takie, aby sprawdzić czy jest i jak działa odpowiednik javowego JAXB (Java Architecture for XML Binding) w języku Python.

Rozwiązań jest z pewnością więcej niż jedno, jednak dla mnie całkiem spoko okazał się moduł dxml, głównie z uwagi na banalnie wyglądające api. Poza tym w ferworze walki z projektem Sterownika domowego, nie chciałem poświęcać więcej czasu na research, a dmxl był na szczycie listy wyszukiwania w google. 🙂

Instalacja modułu dexml może być wykonana z wykorzystaniem narzędzia pip, w systemie Linux uruchomionego z uprawnieniami admina.

sudo pip3 install dexml

Pierwszy krok w mapowaniu to określenie modelu danych. W moim przykładzie powstały klasy reprezentujące test wyboru (obiekt klasy Test), zawierający w sobie m. in. listę pytań (obiekty klasy Question). Każde pytanie ma przypisane cztery różne odpowiedzi (obiekty klasy Answer), z czego tylko jedna jest poprawna. Jak w szkole :] Zrobię krok w przód i pokażę już w tym miejscu wynik działania skryptu, aby łatwiej zobrazować o co chodzi.

<?xml version="1.0" ?>
<Test domain="Historia">
    <amount>2</amount>
    <Questions>
        <Question value="W którym roku została zrobiona pierwsza żarówka przez Tomasza Edisona?" correct="1879">
            <Answers>
                <Answer value="1795" />
                <Answer value="1892" />
                <Answer value="1879" />
                <Answer value="1920" />
            </Answers>
        </Question>
        <Question value="Kto powiedział 'kości zostały rzucone'?" correct="Juliusz Cezar">
            <Answers>
                <Answer value="Kleopatra" />
                <Answer value="Juliusz Cezar" />
                <Answer value="Marek Antoniusz" />
                <Answer value="Hamurabi" />
            </Answers>
        </Question>
    </Questions>
</Test>

Co tu się dzieje? Tag Test będący elementem głównym pliku XML, zawiera atrybut domain, który reprezentuje dziedzinę, której ten test dotyczy (w przykładzie Historia). Test składa się z dwóch pytań (tagi Question, których rodzicem jest element Questions). Treść pytania przechowywana jest w atrybucie value, natomiast poprawna odpowiedź w tym samym tagu, w atrybucie correct. Wszystkie odpowiedzi przechowywane są w elemencie potomnym po Question, czyli zbiorczym tagu Answers. Każda pojedyncza odpowiedź jest przechowywana w atrybucie value znacznika Answer. W skrócie, zamieszanie z poplątaniem:]

#!/usr/bin/python3
import dexml
from dexml import fields
 
class Answer(dexml.Model):
    value = fields.String() #1
 
class Question(dexml.Model):
    value = fields.String()
    answers = fields.List(Answer, tagname='Answers') #2
    correct_answer = fields.String(attrname="correct") #3
 
class Test(dexml.Model):
    domain = fields.String()
    amount_of_questions = fields.Integer(tagname = "amount") #4
    questions = fields.List(Question, tagname='Questions')
 
    def get_correct_results(self): #5
        results = []
        for question in self.questions:
            results.append(question.value + " (" + question.correct_answer+")")
        return results
 
    def give_question(self): #6
        results = []
        temp_result = ""
        for question in self.questions:
            temp_result = question.value + "\n"
            for answer in question.answers:
                temp_result += answer.value + "\n"
            results.append(temp_result)
        return results

To jak opisałem powyżej plik XML ma odzwierciedlenie w klasach modelu w języku Python. W #1 znajduje się mapowanie wartości value na znacznik <Answer>, który będzie przechowywał ciąg znaków. Nazwa klasy (Answer) określa w tym przypadku nazwę elementu w pliku XML.

W klasie Question dzieje się już coś ciekawszego, mianowicie w #2 definiujemy, że plik wyjściowy będzie zawierał tag zbiorczy Answers, którego potomkami będą obiekty klasy Answer, w liczbie większej niż jeden.

W #3 widać różnicę w sposobie określenia czy składnik klasy ma być odwzorowany w postaci znacznika czy jako atrybut znacznika głównego określanego przez nazwę klasy. W tym przypadku prawidłową odpowiedź (correct_answer) chcę przechowywać jako atrybut elementu <Question>, co uzyskuję dzięki nadaniu wartości parametrowi attrname.

W #4, na przykładzie liczby pytań (amount_of_questions), chciałem pokazać możliwość mapowania na typy inne niż łańcuchy znaków, tutaj Integer. Próba przypisania do pola klasy wartości tekstowej spowoduje wygenerowanie błędu.

ValueError: invalid literal for int() with base 10: 'coś innego niż liczba'

Metody klasy Testget_correct_results() oraz give_question() (#5 oraz #6) służą do prezentacji danych przechowywanych w modelu, będą wykorzystywane do wypisania na ekranie danych z pliku XML.

Model danych już jest, w dalszej kolejności trzeba go uzupełnić, jednak nie ma w tym nic skomplikowanego, bo to zwykłe tworzenie obiektów i ustawianie atrybutów. Gdy wszystko jest gotowe, wywołanie test.render() robi całą brudną robotę, generując oczekiwany wynik w formie XML.

#!/usr/bin/python3
import dexml
from models import Answer, Question, Test
 
def generate_xml():
    q1 = Question(value="W którym roku została zrobiona pierwsza żarówka przez Tomasza Edisona?")
    q1aA = Answer(value="1795")
    q1aB = Answer(value="1892")
    q1aC = Answer(value="1879")
    q1aD = Answer(value="1920")
    q1.answers.append(q1aA)
    q1.answers.append(q1aB)
    q1.answers.append(q1aC)
    q1.answers.append(q1aD)
    q1.correct_answer = q1aC.value
 
    q2 = Question(value="Kto powiedział 'kości zostały rzucone'?")
    q2aA = Answer(value="Kleopatra")
    q2aB = Answer(value="Juliusz Cezar")
    q2aC = Answer(value="Marek Antoniusz")
    q2aD = Answer(value="Hamurabi")
    q2.answers.append(q2aA)
    q2.answers.append(q2aB)
    q2.answers.append(q2aC)
    q2.answers.append(q2aD)
    q2.correct_answer = q2aB.value
 
    test = Test()
    test.questions.append(q1)
    test.questions.append(q2)
    test.domain = "Historia"
    test.amount_of_questions = len(test.questions)
 
    return test.render()

Wynik metody generate_xml() przechwytuję do zmiennej content, aby w dalszej kolejności zapisać go do pliku. Sam zapis, bez żadnych bajerów, najprościej jak się da, bez obsługi sytuacji wyjątkowych, itp., bo nie o to chodzi w tym przykładzie. Plik pojawił się na dysku, w związku z czym mogę spróbować zrobić mapowanie w drugą stronę. Tym razem pobieram zapisaną zawartość i metodą parse(), przekształcam do formy obiektowej. Aby potwierdzić poprawność działania mapowania, wypisuję zawartość testu na ekranie, wykorzystując wspomniane wcześniej metody klasy Test.

    content = generate_xml()
 
    file = open('test_history.xml', 'w')
    file.write(content)
    file.close()
 
    file = open('test_history.xml', 'r')
    from_file = file.read()
    file.close()
 
    test = Test.parse(from_file)
 
    print("Pytania z testu:")    
    for q in test.give_question():
        print(q)
 
    print("Prawidłowe odpowiedzi:")
    for r in test.get_correct_results():
        print(r)

Wynik działania powyższego skryptu:

Pytania z testu:
W którym roku została zrobiona pierwsza żarówka przez Tomasza Edisona?
1795
1892
1879
1920
 
Kto powiedział 'kości zostały rzucone'?
Kleopatra
Juliusz Cezar
Marek Antoniusz
Hamurabi
 
Prawidłowe odpowiedzi:
W którym roku została zrobiona pierwsza żarówka przez Tomasza Edisona? (1879)
Kto powiedział 'kości zostały rzucone'? (Juliusz Cezar)

Jak widać, mapowanie obiektów na XML w Pythonie jest całkiem przyjemne, przynajmniej w takim prostym przykładzie. Wystarczy dobrze przygotowany model i dwie metody: parse() oraz render().

Literatura obowiązkowa

dexml: a dead-simple Object-XML mapper for Python

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *