TI/Podstawy XML i jego parsowanie

Z Brain-wiki

XML

Nazwa XML jest skrótem od (ang. Extensible Markup Language), co w wolnym tłumaczeniu oznacza "rozszerzalny język znaczników". Sam XML jest uniwersalnym językiem przeznaczonym do reprezentowania różnych danych w strukturalizowany sposób.


XML nie jest językiem programowania i nie trzeba być programistą, by z niego korzystać. XML sprawia, że generowanie danych i ich odczyt są znacznie łatwiejsze, zapewniając strukturę łatwą do generacji i odczytu programowego. XML jest rozszerzalny i niezależny od platformy, i wykorzystuje Unicode do reprezentacji tekstu.

XML korzysta z tagów (słowa ujęte w '<' i '>') oraz atrybutów (w postaci nazwa="wartość"), bardzo podobnie do HTMLa. Jednakowoż HTML definiuje dokładne znaczenia każdego z tagów i jego atrybutów, jak również ich wygląd w przeglądarce, XML używa tagów tylko do rozgraniczenia pewnej części z całego dokumentu, a interpretację znaczenia pozostawia aplikacji odczytujacej te dane. I tak "<p>" w dokumencie XML-owym nie oznacza początku paragrafu. W zależności od kontekstu, czy konstrukcji formatu danych taki tag może oznaczać dowolny parametr zaczynający się na "p" albo na inną literę alfabetu.

Pliki XML-owe tak samo jako HTML-owe są plikami tekstowymi. Dzięki temu można przeglądać dane bez potrzeby korzystania z dodatkowych aplikacji, a w ostateczności edytować je korzystając z edytora tekstu. Postać tekstowa umożliwia też łatwiejsze przeglądanie tekstu w celu usuwania błędów.

W przeciwieństwie do HTML, reguły dotyczące plików XML są ścisłe i nie naruszalne. Źle napisany tag, zgubiony nawias czy atrybut nie ujęty w cudzysłów czyni plik XML bezużytecznym, podczas gdy w HTML taka praktyka jest dozwolona. Oficjalna specyfikacja języka XML zabrania aplikacjom domyślać się co ma znaczyć dany fragment uszkodzonego plik XML; jeśli w pliku jest błąd program powinien wstrzymać wykonywanie i zgłosić błąd.

Ze względu na postać tekstową danych i rozgraniczanie ich za pomocą tekstowych znaczników, pliki XML-owe są znacząco większe niż pliki biarne z takimi samymi danymi.

XML pozwala na zdefiniowanie nowego formatu poprzez łączenie lub korzystanie z innego. Ponieważ różne formaty są tworzone zupełnie niezalenie, mogą mieć tagi lub atrybuty o takiej samej nazwie, co powoduje dwuznaczność przy łączeniu takich dokumentów (np. w jednym "<p>" oznacza "paragraf", a w innym "pisarza"). W celu wyeliminowania takiej dwuznaczności w XML wprowadzona mechanizm przestrzeni nazw.(...)XML Schema został zaprojektowany, by odzwierciedlać to wsparcie dla modularności na poziomie definiowania struktury dokumentu XML, ułatwiając połaczeniu dwóch schematów w celu stworzenia trzeciego, który obejmuje strukturę połaczonych dokumentów.

(XML in 10 points, W3C Communications Team tłumaczenie Jacek Gleń)

Chwila refleksji

Zatrzymajmy się na chwilę, żeby się z oswoić z wyglądem pliku xml prosciutkiego i bardziej skomplikowanego. Podziwiajmy przez chwilę drzewiastą strukturę tych plików, popatrzmy jakie są zasadnicze różnice między nimi.

Parsery

lxml.etree

Moduł lxml jest pythonową nakładką na na biblioteki libxml2 i libxslt napisane w języku C. Jest o tyle przyjemna, że łączy szybkość działania i kompletność z naturalnym dla nas interfejsem pythonowym i jest kompatybilna z ElementTree.

Poniższe przykłady są częściowo zaczerpnięte z samouczka do lxml.etree autorstwa Stefana Behnela.

import

Zazwyczaj piszemy:

from lxml import etree

W przypadku, gdy program nie będzie korzystał z funkcjonalności dostępnej wyłącznie w lxml.etree import można zrobić w następujący sposób:

try:
  from lxml import etree
  print "running with lxml.etree"
except ImportError:
  try:
    # Python 2.5
    import xml.etree.cElementTree as etree
    print "running with cElementTree on Python 2.5+"
  except ImportError:
    try:
      # Python 2.5
      import xml.etree.ElementTree as etree
      print "running with ElementTree on Python 2.5+"
    except ImportError:
      try:
        # normal cElementTree install
        import cElementTree as etree
        print "running with cElementTree"
      except ImportError:
        try:
          # normal ElementTree install
          import elementtree.ElementTree as etree
          print "running with ElementTree"
        except ImportError:
          print "failed to import ElementTree from any known place"

Klasa Element

Element jest podstawowym kontenerem w ElementTree. W większości wypadków do drzewa XML-owego dostajemy się przez tęże klasę.

Tworzymy obiekt o nazwie root klasy Element:

>>> root = etree.Element("root")

Do XML-owej nazwy znacznika (taga) dostajemy się za pomocą pola tag:

>>> print root.tag
root

Obiekty klasy Element tworzą strukturę drzewa XML-owego. Żeby skontruować dzieci i dodać je do rodzica, można m.in. użyć metody append:

 >>> root.append( etree.Element("child1") )

Można też do tego użyć fabryki SubElement, która wymaga podania nazwy rodzica jako parametru oraz takich samych parametrów, jak fabryka Element.

>>> child2 = etree.SubElement(root, "child2")
>>> child3 = etree.SubElement(root, "child3")

Zobaczmy, jakie drzewo xml-owe stworzyliśmy:

>>> print(etree.tostring(root, pretty_print=True))
<root>
  <child1/>
  <child2/>
  <child3/>
</root>

Elementy są sekwencjami

Żeby było jeszcze prościej, elementy zachowują się jak sekwencje:

>>> child = root[0]
>>> print child.tag
child1

>>> print len(root)
3

>>> root.index(root[1]) # tylko w lxml.etree!
1

>>> children = list(root)

>>> for child in root:
...     print child.tag
child1
child2
child3

>>> root.insert(0, etree.Element("child0"))
>>> start = root[:1]
>>> end   = root[-1:]

>>> print start[0].tag
child0
>>> print end[0].tag
child3

>>> root[0] = root[-1] # zmienia kolejność elementów w drzewie w lxml.etree
>>> for child in root:
...     print child.tag
child3
child1
child2

Elementy mają atrybuty

W XML-u tagi mogą mieć atrybuty. Tagi z atrybutami, można tworzyć bezpośednio za pomocą klasy Element:

>>> root = etree.Element("przedmiot", interesujacy="strasznie")
>>> etree.tostring(root)
b'<przedmiot interesujacy="strasznie"/>'

Dostęp do atrybutów zapewniany jest przez metody set i get:

>>> print root.get("interesting")
None
>>> print root.get("interesujacy")
strasznie
>>> root.set("interesujacy","troszke")
>>> print root.get("interesujacy")
troszke

Za pomocą metody set można tez atrybuty dodawać:

>>> root.set("trudny","troszke")
>>> print root.get("interesujacy")
troszke

Atrybuty można też uzyskać za pomocą metody attrib:

>>> print root.attrib
{'interesujacy': 'troszke', 'trudny': 'troszke'}

Atrybuty są słownikami

Kontynuując poprzedni przykład, attrib zwraca nam słownik atrybutów danego taga:

>>> child = root[0]
>>> child.tag
'child1'
>>> child.set("atr", "1")
>>> print child
<Element child1 at 13943a0>
>>> child.attrib
{'atr': '1'}
>>> child.set("atr2", "2")
>>> child.attrib
{'atr2': '2', 'atr': '1'}

Pliki

Parsery xml jednak najczęściej służą do przetwarzania w jakiś sposób plików, a nie tylko tworzenia struktury xml'owej w pamięci, więc teraz przyjrzymy się prostym przykładom plików:

Odczyt

<root>
<child>One</child>
<child>Two</child>
</root>

Skopiujmy gdzieś ten kawałek, i zapiszmy pod nazwą example.xml.

>>> xml_file = "/sciezka/bezwzgledna/example.xml"
>>> tree = ET.parse(xml_file)
>>> element = tree.getroot()
>>> print element.tag
root
>>> for subelement in element:
	print subelement.text
	
One
Two

Żeby nie mieszało nam się, które słówka są kluczowe, a które możemy samodzielnie wymyślać, przepiszmy ten przykład na język polski:

<korzen>
<dziecko>Pierwsze</dziecko>
<dziecko>Drugie</dziecko>
</korzen>
>>> plik_xml = "/Users/magda/przyklad.xml"
>>> drzewko = ET.parse(plik_xml)
>>> el = drzewko.getroot()
>>> for podelement in el:
	print podelement.text
	
Pierwsze
Drugie
>>> print el.tag
korzen

Wzbogaćmy nasz przykładowy plik o atrybuty w tagach:

<korzen>
<dziecko atrybut="wartosc" atrybut2="wartosc2">Pierwsze</dziecko>
<dziecko>Drugie</dziecko>
</korzen>
from xml.etree import ElementTree as ET

plik_xml = "/Users/magda/przyklad.xml"
drzewko = ET.parse(plik_xml)
el = drzewko.getroot()
for podelement in el:
        print podelement.text
        print podelement.attrib

# Wynik wykonania:
>>>
Pierwsze
{'atrybut2': 'wartosc2', 'atrybut': 'wartosc'}
Drugie
{}

Zapis

Przydatne funkcje

API biblioteki lxml.etree oraz xml.etree są bardzo podobne. Na potrzeby naszych zajęć prawdopodobnie nawet nie zauważymy różnicy. Wygodna dokumentacja jest tu(xml.etree) oraz tu (lxml) Poniżej kilka przykładów.

find / findall

pozwalające wybrać pierwszy/wszystkie tagi o zadanej nazwie:

<korzen>
<dziecko atrybut="wartosc" atrybut2="wartosc2">Pierwsze</dziecko>
<dziecko>Drugie</dziecko>
<atrapa>Atrapa</atrapa>
<atrapa>Atrapa2</atrapa>
<atrapa>Atrapa3</atrapa>
</korzen>

from xml.etree import ElementTree as ET

plik_xml = "/Users/magda/przyklad.xml"
drzewko = ET.parse(plik_xml)
el = drzewko.getroot()
dzieci = el.findall("dziecko")
for d in dzieci:
        print d.text
atrapy = el.findall("atrapa")
for a in atrapy:
        print a.text

# Wynik wykonania:
>>>
Pierwsze
Drugie
Atrapa
Atrapa2
Atrapa3

A ile tagów o nazwie dziecko zostanie znalezione w tym przypadku?:

<korzen>
<dziecko atrybut="wartosc" atrybut2="wartosc2">Pierwsze</dziecko>
<dziecko>Drugie</dziecko>
<atrapa>Atrapa</atrapa>
<atrapa>Atrapa2</atrapa>
<atrapa>Atrapa3</atrapa>
<dziecko>
        <dziecko>
                <dziecko>
                </dziecko>
        </dziecko>
</dziecko>
</korzen>
from xml.etree import ElementTree as ET

plik_xml = "/Users/magda/przyklad.xml"
drzewko = ET.parse(plik_xml)
el = drzewko.getroot()
dzieci = el.findall("dziecko")
print len(dzieci)

# Wynika wykonania:
>>>
3


Tworzenie własnego parsera

Do budowania drzewa, na którym potem wykonujemy wszystkie operacje, jest używany standardowy "parser". Ale czasem chcielibyśmy wykonać jakieś operacje już na etapie budowania drzewa. Mamy taką możliwość, możemy utworzyć własną klasę, której przedefiniujemy odpowiednie metody, i podać ją jako parametr odpowiednim funkcjom. W szczególności w powyższych przykładach dotyczących plików mogliśmy funkcji parse jako drugi parametr podać nasz własny parser:

xml.etree.ElementTree.parse(source, parser=None)
    Parses an XML section into an element tree. source is a filename or file object    containing XML data. parser is an optional parser instance. If not given, the standard XMLParser parser is used. Returns an ElementTree instance.

A oto przykład tworzenia własnego parsera, który przy okazji parsowania zlicza głębokość drzewka:

>>> from xml.etree.ElementTree import XMLParser
>>> class MaxDepth:                     # The target object of the parser
...     maxDepth = 0
...     depth = 0
...     def start(self, tag, attrib):   # Called for each opening tag.
...         self.depth += 1
...         if self.depth > self.maxDepth:
...             self.maxDepth = self.depth
...     def end(self, tag):             # Called for each closing tag.
...         self.depth -= 1
...     def data(self, data):
...         pass            # We do not need to do anything with data.
...     def close(self):    # Called when all data has been parsed.
...         return self.maxDepth
...
>>> target = MaxDepth()
>>> parser = XMLParser(target=target)
>>> exampleXml = """
... <a>
...   <b>
...   </b>
...   <b>
...     <c>
...       <d>
...       </d>
...     </c>
...   </b>
... </a>"""
>>> parser.feed(exampleXml)
>>> parser.close()
4

Pamiętaj, że gdy używamy argumentu target, XMLParser() nie zwraca automatycznie drzewa -- jesli chcemy, musimy sami je zbudować w naszej klasie, którą podajemy jako target.

Ćwiczenie

Napisz parser, który podczas budowania drzewa zlicza ilość elementów o nazwie child Przetestuj jego działanie.

Zadania

Termin oddania projektów do 15-tego czerwca!
Zmiany w terści zadań, wynikające z uwag studentów, akceptowane są do 1-go czerwca!

Wstęp do projektu zaliczeniowego: Temat 1 — tworzenie klas na podstawie opisu w XML

Dynamiczna generacja klas

Python umożliwia dynamiczną generację klas.

Można to robić na dwa sposoby. W funkcji:

def klasa_p(p):
  class P(object):
     x = p
  return P
P_11 = klasa_p(11)
p_11 = P_11()
assert p_11.x == 11

Przy normalnej definicji klasy, wykonuje się sekwencja poleceń. Na końcu bloku class otrzymujemy pewien zestaw zmiennych w przestrzeni nazw (namespace), który jest słownikiem. Można też od razu stworzyć swój namespace, w tym ypadku pewien słownik i za pomocą funkcji type klasę o odpowieniej nazwie.

B = type('B', (object,), dict(x=11))
assert B().x == 11

Argumentami funkcji type są — nazwa klasy, lista rodziców i treść klasy, czyli słownik. czyli de facto jej namespace. Wiedząc, że w pythonie można dynamicznie tworzyć klasy tak jak w poniższym przykładzie:

I kolejne przykłady:

  • class C(object):
      def f(self, x): return x
      f = lambda x: None
    assert C().f(33) == None
    
  • par = "self.y * self.y"
    x = "y"
    d={}
    d[x] = 10
    d["action"] = lambda self: eval(par)
    Bar = type('Bar',(object,),d)
    b = Bar()
    print b.y
    print b.action()
    
Treść projektu

Napisz prosty program, który z zadanego opisu XML tworzy klasę. Opis XML może wyglądać np. tak:

<class nazwa="Test">
<attribute nazwa="y" wartosc="10">
</attribute>
<attribute nazwa="x" wartosc="5">
</attribute>
<method nazwa="action" tresc="self.y * self.y">
</method>
<method nazwa="metoda2" tresc="self.x * self.y">
</method>
</class>

W znaczniku class w atrybucie nazwa jest zapisana nazwa klasy. W znacznikach o nazwie "attribute" w atrybucie "nazwa" jest zapisana nazwa atrybutu klasy, w atrybucie "wartosc" -- jego wartosc. W znacznikach "method" w atrybucie "nazwa" jest nazwa metody, a atrybucie "tresc" -- jej tresc. Dopuszczamy wyłącznie bardzo proste metody, bezparametrowe, operujące na atrybutach obiektu i wyłącznie zwracające wynik tych operacji. Nieprecyzyjności w sformułowaniu należy zinterpretować na własną korzyść, tzn tak, jak nam ławiej zaimplementować rozwiązanie.

Projekt zaliczeniowy: Temat 1 -- tworzenie klas na podstawie opisu w XML

Rozwiń powyższe zadanie.

  • Zaprojektuj sposób opisu klasy, uwzględniający możliwie dużo aspektów nie objętych obecną wersją, np.:
    • metody z argumentami
    • zmianę metody init
    • metody, które coś wypisują
    • oddzielnie metody klasowe od metod obiektu
    • oddzielnie atrybuty klasowe od atrybutów obiektu
    • dziedziczenie

...

Sposób opisu zaprojektowany przez Ciebie może być zupełnie inny niż zaproponowany we wstępie do zadania. Napisz parser, który na podstawie opisu XML zgodnego z Twoimi "zasadami" zaimplementuje opisane klasy.


FAQ do Tematu 1

Wstęp do projektu zaliczeniowego: Temat 2

Projekt zaliczeniowy: Temat 2 -- Parsowanie pliku z metaopisem danych

Rozwiń swoją klasę do cięcia danych z pracowni o automatyczne wczytywanie wszystkich możliwych parametrów, typu częstość próbkowania, nazwy/ilość elektrod z zadanego pliku .xml Format pliku .xml -- tak jak pliki .info z pracowni. Przy ocenie zadania będzie brana pod uwagę dokładność implementacji: odporność na błędy, łapanie wyjątków, jakość dokumentacji, itd

FAQ do Tematu 2

Literatura

Korzystałyśmy z:

  1. XML in 10 points
  2. samouczek do lxml.etree