Čtyři způsoby zpracování XML v Pythonu

17. 2. 2022
Doba čtení: 28 minut

Sdílet

 Autor: Depositphotos
Existuje poměrně velké množství způsobů a knihoven, jakými je možné v jazyku Python manipulovat s daty uloženými v XML. Dnes si představíme čtyři různé způsoby načítání XML, přičemž každý se hodí pro jiné účely.

Obsah

1. Čtyři způsoby zpracování XML v×Pythonu

2. Testovací data ve formátu XML

3. Knihovna lxml

4. Příklad použití knihovny lxml

5. Přístup k atributům a poduzlům naparsovaného dokumentu

6. Knihovna xmltodict

7. Načtení XML s převodem do slovníku

8. Zpracování dat uložených ve slovníku

9. Převod dat z XML do formátu JSON

10. Knihovna untangle

11. Načtení XML s následným převodem do objektu

12. Použití standardní knihovny xml.sax

13. Přečtení podrobnějších informací o uzlu

14. Programová filtrace uzlů podle jejich typu

15. Zobrazení informací o poduzlech vybraných uzlů

16. Chování knihovny xml.sax v případě nevalidního XML

17. Odkazy na Internetu

1. Čtyři způsoby zpracování XML v Pythonu

Existuje poměrně velké množství způsobů a knihoven, jakými je možné v programovacím jazyku Python manipulovat s daty uloženými ve formátu XML. Pokusme se tedy tyto mnohé přístupy rozdělit do čtyř skupin podle toho, o jak rozsáhlá data se jedná, protože některé z dále uvedených způsobů jsou sice po programátorské stránce velmi příjemné na použití (s XML se například dá pracovat jako s běžným pythonovským slovníkem popř. dokonce jako s plnohodnotným objektem), ovšem pro větší množství dat zcela nevhodné, a to kvůli vysokým kvůli nárokům na objem operační paměti popř. na požadovaný výkon mikroprocesoru(ů):

  1. Velmi často se setkáme s průběžným „proudovým“ zpracováním dat načítaných z XML, resp. přesněji řečeno jednotlivých uzlů (elementů) tak, jak je získává XML parser. Informace o jednotlivých uzlech jsou do uživatelského kódu většinou posílány formou událostí (events), které jsou zpracovávány zaregistrovanými funkcemi nebo metodami. Tento přístup se nazývá Simple API for XML neboli zkráceně SAX (toto jméno naznačuje, že se jedná o formalizovaný přístup, ovšem není tomu tak – v každém jazyku může být způsob proudového zpracování XML realizován odlišně). Tímto způsobem lze zpracovávat údaje uložené v XML s prakticky neomezenou velikostí.
  2. Vytvoření reprezentace celého XML souboru ve formě obecného stromu, přičemž pro přístup k jednotlivým uzlům lze použít Document Object Model neboli DOM (podobně jako při manipulaci s obsahem HTML stránek). Nároky na potřebnou kapacitu operační paměti jsou v tomto případě vyšší, než v předchozím případě, takže se tento způsob využívá především ve chvíli, kdy je nutné „náhodně“ přistupovat k uzlům stromu, popř. kdy je zapotřebí použít nějaké složitější mechanismy pro výběr většího množství uzlů.
  3. Načtení XML a jeho následná transformace do podoby slovníku (dictionary) jazyka Python. Jedná se o velmi podobný přístup, jaký je například použit při deserializaci dat uložených ve formátu JSON. Může se jednat o vhodný způsob ve chvíli, kdy má například nějaká služba akceptovat a zpracovávat poměrně malé objemy dat ve formátech XML i JSON (například některé webové služby akceptují popř. produkují oba tyto formáty). Popř. je možné tímto způsobem zpracovávat konfigurační soubory atd.
  4. Načtení XML a jeho následná transformace do podoby objektu (object) jazyka Python. Tento objekt typicky obsahuje informace o kořenovém uzlu XML a navíc i atributy s nejbližšími poduzly a atributy samotného kořenového uzlu. Jedná se o ideální způsob použitelný ve chvíli, kdy je nutné pracovat s relativně malými konfiguračními soubory atd. („relativně malé“ přitom označuje velikost, která se v praxi neustále zvětšuje).

V dnešním článku si ve stručnosti představíme všechny čtyři způsoby zpracování dat uložených v XML souborech. Navážeme tak na dvojici článků o knihovně LXML.

Poznámka: existují ještě další způsoby rozdělení přístupu k datům uložených v souborech XML. Zejména je nutné odlišit zpracování validních XML, dále XML nevalidních (ale po syntaktické stránce korektních), XML bez schématu, XML s větším množstvím jmenných prostorů atd.

2. Testovací data ve formátu XML

V demonstračním příkladech budeme používat několik XML souborů. Zejména vývojáři pracující s programovacím jazykem Java a systémem Maven se určitě již mnohokrát setkali se soubory nazvanými pom.xml. Jen ve stručnosti si řekněme, že tyto soubory obsahují informace o projektu a samozřejmě i konfiguraci celého projektu, popř. další informace používané některými Maven pluginy. Tyto soubory jsou primárně využívané nástrojem Apache Maven, přičemž zkratka POM znamená Project Object Model. Ve skutečnosti ovšem s těmito soubory pracuje i mnoho dalších nástrojů, ať již se jedná o integrovaná vývojová prostředí či o specializovanější nástroje určené pro kontrolu závislých knihoven, licencí použitých v knihovnách atd. atd.

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.tisnik.uberproject.test</groupId>
  <artifactId>test-app-junit-dependency</artifactId>
  <version>1.0</version>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
    </dependency>
    <dependency>
      <groupId>foo</groupId>
      <artifactId>foo</artifactId>
      <version>1.0.0</version>
    </dependency>
    <dependency>
      <groupId>bar</groupId>
      <artifactId>bar</artifactId>
      <version>1.2.3</version>
    </dependency>
  </dependencies>
</project>
Poznámka: tento soubor naleznete na GitHubu, konkrétně na adrese https://github.com/tisnik/lxml-examples/blob/master/pom.xml.

Druhý soubor založený na XML obsahuje reprezentaci jednoduchého stromu s kořenem, třemi uzly navázanými na kořen, kde každý z těchto uzlů obsahuje tři listy. Všechny uzly v XML přitom mají nastaveny atributy, k nimž budeme programově přistupovat:

<root atribut1="1" attribut2="2" popis="koren">
  <left popis="levy vnitrni poduzel">
    <left popis="list zcela nalevo"/>
    <middle popis="list"/>
    <right popis="list"/>
  </left>
  <middle popis="prostredni vnitrni poduzel">
    <left popis="list"/>
    <middle popis="prostredni list"/>
    <right popis="list"/>
  </middle>
  <right popis="pravy vnitrni poduzel">
    <left popis="list"/>
    <middle popis="list"/>
    <right popis="list zcela napravo"/>
  </right>
</root>

A konečně třetí XML soubor obsahuje popis databázového schématu vygenerovaný nástrojem SchemaSpy. Jedná se o velmi jednoduchou databázi se třemi tabulkami:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<database name="test1" schema="public" type="PostgreSQL - 9.6.10">
   <tables>
      <table name="department" numRows="0" remarks="" schema="public" type="TABLE">
         <column autoUpdated="false" defaultValue="null" digits="0" id="0" name="id" nullable="false" remarks="" size="10" type="int4" typeCode="4"/>
         <column autoUpdated="false" defaultValue="null" digits="0" id="1" name="name" nullable="false" remarks="" size="20" type="varchar" typeCode="12"/>
         <column autoUpdated="false" defaultValue="null" digits="0" id="2" name="location" nullable="false" remarks="" size="20" type="varchar" typeCode="12"/>
      </table>
      <table name="employee" numRows="0" remarks="" schema="public" type="TABLE">
         <column autoUpdated="false" defaultValue="null" digits="0" id="0" name="id" nullable="false" remarks="" size="10" type="int4" typeCode="4"/>
         <column autoUpdated="false" defaultValue="null" digits="0" id="1" name="name" nullable="false" remarks="" size="20" type="varchar" typeCode="12"/>
         <column autoUpdated="false" defaultValue="null" digits="0" id="2" name="job" nullable="false" remarks="" size="20" type="varchar" typeCode="12"/>
         <column autoUpdated="false" defaultValue="null" digits="0" id="3" name="manager" nullable="true" remarks="" size="10" type="int4" typeCode="4"/>
         <column autoUpdated="false" defaultValue="null" digits="0" id="4" name="hiredate" nullable="false" remarks="" size="13" type="date" typeCode="91"/>
         <column autoUpdated="false" defaultValue="null" digits="0" id="5" name="salary" nullable="false" remarks="" size="10" type="int4" typeCode="4"/>
         <column autoUpdated="false" defaultValue="null" digits="0" id="6" name="comment" nullable="true" remarks="" size="10" type="int4" typeCode="4"/>
         <column autoUpdated="false" defaultValue="null" digits="0" id="7" name="department" nullable="false" remarks="" size="10" type="int4" typeCode="4"/>
      </table>
      <table name="project" numRows="0" remarks="" schema="public" type="TABLE">
         <column autoUpdated="false" defaultValue="null" digits="0" id="0" name="id" nullable="false" remarks="" size="10" type="int4" typeCode="4"/>
         <column autoUpdated="false" defaultValue="null" digits="0" id="1" name="employee" nullable="false" remarks="" size="10" type="int4" typeCode="4"/>
         <column autoUpdated="false" defaultValue="null" digits="0" id="2" name="startdate" nullable="false" remarks="" size="13" type="date" typeCode="91"/>
         <column autoUpdated="false" defaultValue="null" digits="0" id="3" name="enddate" nullable="false" remarks="" size="13" type="date" typeCode="91"/>
      </table>
   </tables>
</database>

3. Knihovna lxml

Na úvod si připomeneme některé možnosti nabízené knihovnou lxml. Tato knihovna, s jejímiž základy se v této kapitole seznámíme, slouží k načítání (parsování) XML souborů, dále pro přístup k jednotlivým prvkům výsledného stromu, tvorbě a zapisování nových XML a v případě potřeby lze tuto knihovnu použít i pro zpracování HTML stránek. Zajímavé je, že se tato knihovna poměrně dobře hodí i pro práci s nevalidními XML, XML bez schématu, XML s více jmennými prostory atd. – tj. se soubory, které může být obtížné zpracovat v jiných nástrojích. Vývojářům jsou v případě potřeby k dispozici i další zajímavé technologie, zejména XPath (zjednodušeně: přístup k elementům a jejich atributům přes doménově specifický jazyk) a již výše zmíněný SAX, tj. možnost zpracovávat XML jako sekvenci elementů, což je přístup mnohem méně náročný na paměť. Navíc se většinou jedná o rychlejší způsob práce s XML.

Na knihovnu lxml se můžeme dívat jako na vhodný doplněk ke knihovnám libxml2 a libxslt, pro které samozřejmě existují příslušná rozhraní pro Python. Tyto knihovny jsou především rychlé a nabízí prakticky všechny užitečné operace pro práci s XML. Na druhou stranu se jedná o spíše nízkoúrovňové knihovny poměrně přesně kopírující céčkové rozhraní, což některým uživatelům Pythonu nemusí plně vyhovovat. Navíc – jelikož se skutečně jedná o relativně tenkou vrstvu mezi programovacím jazykem C a Pythonem – může poměrně snadno dojít k pádům celé aplikace (segfault), což je velmi nepříjemné, zejména při produkčním nasazení. Mj. i z těchto dvou důvodů vznikla knihovna lxml, která je více „pythonovská“ a tudíž snadněji použitelná. Za snadnost použití však někdy zaplatíme pomalejším zpracováním XML, takže záleží na tom, jak velké soubory a v jakém množství se mají zpracovávat.

V případě, že v Pythonu vytváříte aplikace používající další moduly (knihovny), máte již s velkou pravděpodobností knihovnu lxml ve svém systému nainstalovanou. O tom, zda je knihovna skutečně nainstalovaná a dostupná (interpret ji nalezne), se můžete snadno přesvědčit, a to buď příkazem pip3 show lxml nebo pip3 list | grep lxml (což ovšem není tak přesné):

$ pip3 show lxml
 
---
Name: lxml
Version: 3.3.3
Location: /usr/lib/python3/dist-packages
Requires:

Jen pro zajímavost (pip3 show je ovšem lepší řešení):

$ pip3 list | grep lxml
lxml (3.3.3)
Poznámka: mimochodem – verze 3.3.3 zobrazená na výpisu nahoře je již dnes zastará, takže by se měl provést update na verzi 4.x.x (nejnovější stabilní verze je v současnosti verze 4.7.1):
$ sudo pip3 install lxml -U
Collecting lxml
  Downloading https://files.pythonhosted.org/packages/03/a4/9eea8035fc7c7670e5eab97f34ff2ef0ddd78a491bf96df5accedb0e63f5/lxml-4.7.1-cp38-cp38m-manylinux1_x86_64.whl (5.8MB)
    100% |████████████████████████████████| 5.8MB 273kB/s
Installing collected packages: lxml
  Found existing installation: lxml 3.3.3
    Uninstalling lxml-3.3.3:
      Successfully uninstalled lxml-3.3.3
Successfully installed lxml-4.7.1

Nyní znovu zkontrolujeme verzi nainstalované knihovny:

$ pip3 show lxml
 
Name: lxml
Version: 4.7.1
Summary: Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API.
Home-page: http://lxml.de/
Author: lxml dev team
Author-email: lxml-dev@lxml.de
License: BSD
Location: /usr/lib64/python3.8/site-packages
Requires:

Pokud z nějakého důvodu není knihovna lxml nainstalovaná, je její instalace většinou otázkou několika sekund. Na výpisu níže je ukázána instalace této knihovny určené pro Python 2 (používá se tedy příkaz pip a nikoli pip3):

$ pip install --user lxml
 
  Downloading https://files.pythonhosted.org/packages/e5/14/f4343239f955442da9da1919a99f7311bc5627522741bada61b2349c8def/lxml-4.7.1-cp27-cp27mu-manylinux1_x86_64.whl (5.8MB)
    100% |████████████████████████████████| 5.8MB 89kB/s
Installing collected packages: lxml
Successfully installed lxml-4.7.1

4. Příklad použití knihovny lxml

Podívejme se nyní na velmi jednoduchý příklad použití knihovny lxml. V následujícím skriptu využijeme soubor „test5.xml“, který byl popsán ve druhé kapitole. Tento soubor obsahuje strom s kořenem a třemi dalšími uzly, z nichž každý obsahuje tři koncové uzly a mělo by být možné ho knihovnou lxml bez problémů načíst a zpracovat. Výsledkem bude objekt představující rekonstruovaný strom:

import lxml.etree as ET
 
xml = "test5.xml"
tree = ET.parse(xml)
 
root = tree.getroot()
print(ET.tostring(root))

Takto se vypíše rekonstruovaný strom začínající kořenovým uzlem, který jsme získali metodou getroot::

b'<root atribut1="1" attribut2="2" popis="koren"><left popis="levy vnitrni poduzel"><left popis="list zcela nalevo"/><right popis="list"/></left><right popis="pravy vnitrni poduzel"><left popis="list"/><right popis="list zcela napravo"/></right></root>'

5. Přístup k atributům a poduzlům naparsovaného dokumentu

Ve chvíli, kdy máme k dispozici objekt představující kořen stromu vzniklého parsingem XML souboru, je možné postupně začít získávat atributy kořenového uzlu, jeho text a samozřejmě i potomky, tj. uzly ležící o jednu úroveň níže. Přístup k atributům:

print(root.get("atribut1"))
print(root.get("popis"))

Získání potomků:

children = root.getchildren()

Tato metoda obecně vrací sekvenci, takže se k jednotlivým potomkům dostaneme například přes programovou smyčku typu for-each:

for child in children:
    print(child.get("popis"))

Celý demonstrační příklad, který zpracuje jednoduchý XML soubor a vypíše atributy kořenového uzlu i jeho potomky (resp. přesněji řečeno atribut „popis“ potomků), bude vypadat následovně:

import lxml.etree as ET
 
xml = "test5.xml"
tree = ET.parse(xml)
 
root = tree.getroot()
# print(ET.tostring(root))
 
print(root.get("atribut1"))
print(root.get("popis"))
 
children = root.getchildren()
 
for child in children:
    print(child.get("popis"))

Výsledkem bude následujících pět řádků vypsaných na standardní výstup:

1
koren
levy vnitrni poduzel
prostredni vnitrni poduzel
pravy vnitrni poduzel

6. Knihovna xmltodict

Jak jsme si již řekli v úvodní kapitole, je možné data uložená v souborech ve formátu XML zpracovávat několika různými způsoby. Nyní si ukážeme způsob, který se do značné míry podobá deserializaci dat uložených ve formátu JSON. Tento způsob spočívá v tom, že se obsah XML načte a postupně ztransformuje do slovníku (dictionary), což je jedna ze základních datových struktur programovacího jazyka Python. Velká přednost tohoto přístupu spočívá ve snadnosti práce s výsledným slovníkem, nevýhodou pak obecně větší paměťové nároky (v porovnání se SAX, viz další text) a taktéž fakt, že korespondence mezi původním XML a výsledným slovníkem nemusí být pro vývojáře zcela zřejmá (protože vyjadřovací schopnosti XML jsou v tomto ohledu vyšší, než je tomu v případě JSONu, což je výhoda a nevýhoda současně).

Pro načtení XML do slovníku slouží knihovna pojmenovaná xmltodict, která není součástí standardní knihovny programovacího jazyka Python, takže ji budeme muset doinstalovat:

$ pip3 install --user xmltodict
 
Collecting xmltodict
  Downloading xmltodict-0.12.0-py2.py3-none-any.whl (9.2 kB)
Installing collected packages: xmltodict
Successfully installed xmltodict-0.12.0

7. Načtení XML s převodem do slovníku

Po instalaci se můžeme pokusit načíst soubor test5.xml, který jsme v rámci předchozích kapitol zpracovali knihovnou lxml. Celý skript bude nyní značně krátký, protože pouze postačuje otevřít soubor s XML a následně použít funkci parse z balíčku xmltodict:

import xmltodict
 
with open("test5.xml", "r") as fin:
    s=xmltodict.parse(fin.read())
 
    print(s)

Výsledek bude vypadat následovně:

OrderedDict([('root', OrderedDict([('@atribut1', '1'), ('@attribut2', '2'),
('@popis', 'koren'), ('left', OrderedDict([('@popis', 'levy vnitrni poduzel'),
('left', OrderedDict([('@popis', 'list zcela nalevo')])), ('middle',
OrderedDict([('@popis', 'list')])), ('right', OrderedDict([('@popis',
'list')]))])), ('middle', OrderedDict([('@popis', 'prostredni vnitrni
poduzel'), ('left', OrderedDict([('@popis', 'list')])), ('middle',
OrderedDict([('@popis', 'prostredni list')])), ('right',
OrderedDict([('@popis', 'list')]))])), ('right', OrderedDict([('@popis', 'pravy
vnitrni poduzel'), ('left', OrderedDict([('@popis', 'list')])), ('middle',
OrderedDict([('@popis', 'list')])), ('right', OrderedDict([('@popis', 'list
zcela napravo')]))]))]))])

Což zajisté není příliš čitelné, takže si pomůžeme standardní knihovnou pprint:

import xmltodict
import pprint
 
with open("pom.xml", "r") as fin:
    s=xmltodict.parse(fin.read())
 
    pprint.pprint(s)

S následujícím výsledkem:

OrderedDict([('root',
              OrderedDict([('@atribut1', '1'),
                           ('@attribut2', '2'),
                           ('@popis', 'koren'),
                           ('left',
                            OrderedDict([('@popis', 'levy vnitrni poduzel'),
                                         ('left',
                                          OrderedDict([('@popis',
                                                        'list zcela nalevo')])),
                                         ('middle',
                                          OrderedDict([('@popis', 'list')])),
                                         ('right',
                                          OrderedDict([('@popis', 'list')]))])),
                           ('middle',
                            OrderedDict([('@popis',
                                          'prostredni vnitrni poduzel'),
                                         ('left',
                                          OrderedDict([('@popis', 'list')])),
                                         ('middle',
                                          OrderedDict([('@popis',
                                                        'prostredni list')])),
                                         ('right',
                                          OrderedDict([('@popis', 'list')]))])),
                           ('right',
                            OrderedDict([('@popis', 'pravy vnitrni poduzel'),
                                         ('left',
                                          OrderedDict([('@popis', 'list')])),
                                         ('middle',
                                          OrderedDict([('@popis', 'list')])),
                                         ('right',
                                          OrderedDict([('@popis',
                                                        'list zcela '
                                                        'napravo')]))]))]))])
Poznámka: vidíme, že se používá datový typ OrderedDict ze standardní knihovny Pythonu.

8. Zpracování dat uložených ve slovníku

Přístup ke konkrétnímu uzlu se provádí běžným výběrem ze slovníku:

import xmltodict
import pprint
 
with open("test5.xml", "r") as fin:
    s = xmltodict.parse(fin.read())
 
    p = s["root"]
    m = p["middle"]
 
    pprint.pprint(m)

S výsledkem:

OrderedDict([('@popis', 'prostredni vnitrni poduzel'),
             ('left', OrderedDict([('@popis', 'list')])),
             ('middle', OrderedDict([('@popis', 'prostredni list')])),
             ('right', OrderedDict([('@popis', 'list')]))])

Nyní jsme tedy získali obsah uzlu middle, který je navázán na kořenový uzel. Uzel middle obsahuje atribut popis a tři další poduzly. Povšimněte si, jakým způsobem jsou atributy a poduzly uloženy ve slovníku – formou dvojice, přičemž jméno atributu začíná zavináčem (ten v XML běžně není součástí značky). Přístup přímo k atributu popis je tedy triviální:

import xmltodict
 
with open("test5.xml", "r") as fin:
    s = xmltodict.parse(fin.read())
 
    p = s["root"]["middle"]["@popis"]
 
    print(p)

S očekávaným výsledkem:

prostredni vnitrni poduzel

Atribut tedy rozeznáme snadno:

  1. Jméno (první prvek dvojice) začíná znakem zavináče
  2. Hodnotou je řetězec a nikoli vnořený OrderedDict

Zcela stejným způsobem můžeme zpracovat soubor pom.xml, který byl taktéž popsán ve druhé kapitole:

import xmltodict
import pprint
 
with open("pom.xml", "r") as fin:
    s=xmltodict.parse(fin.read())
 
    pprint.pprint(s)

S výsledkem:

import pprint
pprint.pprint(s)
OrderedDict([('project',
              OrderedDict([('modelVersion', '4.0.0'),
                           ('groupId', 'org.tisnik.uberproject.test'),
                           ('artifactId', 'test-app-junit-dependency'),
                           ('version', '1.0'),
                           ('dependencies',
                            OrderedDict([('dependency',
                                          [OrderedDict([('groupId', 'junit'),
                                                        ('artifactId', 'junit'),
                                                        ('version', '3.8.1')]),
                                           OrderedDict([('groupId', 'foo'),
                                                        ('artifactId', 'foo'),
                                                        ('version', '1.0.0')]),
                                           OrderedDict([('groupId', 'bar'),
                                                        ('artifactId', 'bar'),
                                                        ('version',
                                                         '1.2.3')])])]))]))])

Zde narážíme na jinou vlastnost knihovny xmltodict, a to konkrétně na způsob uložení textu umístěného v elementech. Konkrétně se to textů v elementech groupId, artifactId a version. To, že se jedná o elementy se pozná snadno:

  1. Jméno (první prvek dvojice) nezačíná znakem zavináče
  2. Hodnotou je řetězec a nikoli vnořený OrderedDict

9. Převod dat z XML do formátu JSON

Jakmile jsou data reprezentována slovníkem (který jako své prvky obsahuje další slovníky), je snadné například provést převod dat ze zdrojového formátu XML do formátu JSON. Skript, který takový převod provádí, lze napsat na pouhých několik řádků:

import json
import xmltodict
 
with open("pom.xml", "r") as fin:
    s=xmltodict.parse(fin.read())
    print(json.dumps(s, indent=4))

Pro vstupní soubor pom.xml popsaný ve druhé kapitole získáme tento JSON:

{
    "project": {
        "modelVersion": "4.0.0",
        "groupId": "org.tisnik.uberproject.test",
        "artifactId": "test-app-junit-dependency",
        "version": "1.0",
        "dependencies": {
            "dependency": [
                {
                    "groupId": "junit",
                    "artifactId": "junit",
                    "version": "3.8.1"
                },
                {
                    "groupId": "foo",
                    "artifactId": "foo",
                    "version": "1.0.0"
                },
                {
                    "groupId": "bar",
                    "artifactId": "bar",
                    "version": "1.2.3"
                }
            ]
        }
    }
}
Poznámka: povšimněte si rozdílné sémantiky mezi formáty XML a JSON. JSON je v podstatě kontejner pro slovníky a pole (s omezením jmen klíčů), kdežto XML reprezentuje stromovou strukturu, v níž mají uzly atributy popř. obsahují textová data (cdata). Rozdíl je nejvíce patrný v uzlu dependencies.

10. Knihovna untangle

Další knihovnou, o níž se v dnešním článku musíme zmínit, je knihovna nazvaná untangle. Tato knihovna opět slouží k načtení dat ve formátu XML, ovšem výsledná data nebudou reprezentována slovníky, ale plnohodnotným objektem, se všemi přednostmi a zápory, které toto řešení v praxi přináší. Nejdříve si ukažme, jak se tato knihovna nainstaluje. Nejedná se o nic složitého:

$ pip3 install --user untangle
 
Collecting untangle
  Downloading untangle-1.1.1.tar.gz (3.1 kB)
  Preparing metadata (setup.py) ... done
Building wheels for collected packages: untangle
  Building wheel for untangle (setup.py) ... done
  Created wheel for untangle: filename=untangle-1.1.1-py3-none-any.whl size=3410 sha256=4484f6e2d03f09ed217264afa0f55c82bc75d7f1ab8cfa40cd6fb73f04d61c7e
  Stored in directory: /home/ptisnovs/.cache/pip/wheels/7f/c5/cc/22e3fc6b9f951bbd4dfdc0ebd1aceb3b9ce4dee7d7780e270a
Successfully built untangle
Installing collected packages: untangle
Successfully installed untangle-1.1.1

11. Načtení XML s následným převodem do objektu

Opět si pochopitelně vyzkoušíme, jakým způsobem je možné knihovnu untangle použít v praxi. V prvním příkladu načteme soubor pom.xml, převedeme ho do objektu, objekt vypíšeme (zavolá se jeho metoda __str__) a následně vypíšeme atributy objektu s využitím funkce dir:

import untangle
 
o = untangle.parse("pom.xml")
 
print(o)
 
print(dir(o))

Po spuštění skriptu získáme:

Element <None> with attributes None, children [Element(name = project, attributes = {}, cdata =
 
 
 
 
)] and cdata
 
['project']

Tento objekt tedy obsahuje jediný atribut project, takže prozkoumáme tento objekt:

import untangle
 
o = untangle.parse("pom.xml")
 
p = o.project
print(dir(p))

Výsledky:

['artifactId', 'dependencies', 'groupId', 'modelVersion', 'version']

Přistupovat můžeme i k obsahu uzlů (tedy k textu):

import untangle
 
o = untangle.parse("pom.xml")
 
p = o.project.groupId
print(p.cdata)

S tímto výsledkem:

'org.tisnik.uberproject.test'

Popřípadě lze přistupovat ke všem (pod)elementům uzlu:

import untangle
 
o = untangle.parse("pom.xml")
 
p = o.project
print(p.get_elements())

Výsledek:

[Element(name = modelVersion, attributes = {}, cdata = 4.0.0),
Element(name = groupId, attributes = {}, cdata = org.tisnik.uberproject.test),
Element(name = artifactId, attributes = {}, cdata = test-app-junit-dependency),
Element(name = version, attributes = {}, cdata = 1.0),
Element(name = dependencies, attributes = {}, cdata =
 
 
 
)]

Bližší informace o uzlu dependencies:

import untangle
 
o = untangle.parse("pom.xml")
 
d = o.project.dependencies
 
print(len(d))
print()
 
print(dir(d))
print()
 
for dep in d.dependency:
    print(dir(dep))
 
print()
 
for dep in d.dependency:
    print(dep.artifactId.cdata)

S výsledky:

3
 
['dependency', 'dependency', 'dependency']
 
['artifactId', 'groupId', 'version']
['artifactId', 'groupId', 'version']
['artifactId', 'groupId', 'version']
 
junit
foo
bar
Poznámka: v posledním příkladu jsme zobrazili i obsah uzlů artifactId, a to přístupem k atributu cdata daného objektu.

12. Použití standardní knihovny xml.sax

V úvodní kapitole jsme si řekli, že se velmi často můžeme setkat s průběžným „proudovým“ zpracováním dat načítaných z XML, resp. přesněji řečeno jednotlivých uzlů (elementů) tak, jak je získává XML parser. Informace o jednotlivých uzlech jsou do uživatelského kódu většinou posílány formou událostí (events), které jsou zpracovávány zaregistrovanými funkcemi nebo metodami. Podívejme se nyní na způsob realizace zpracovávání událostí na začátku či na konci každého uzlu. Použijeme přitom standardní knihovnu xml.sax, kterou není zapotřebí explicitně instalovat:

import xml.sax
 
 
class XmlHandler(xml.sax.ContentHandler):
    def startElement(self, name, attributes):
        print("Node:", name)
 
 
parser = xml.sax.make_parser()
parser.setContentHandler(XmlHandler())
 
with open("db.public.xml", "r") as fin:
    parser.parse(fin)

Ze zdrojového kódu tohoto skriptu by mělo být zřejmé, že se metoda startElement bude volat ve chvíli, kdy XML parser narazí na začátek libovolného uzlu. O tom se ostatně můžeme velmi snadno přesvědčit spuštěním tohoto skriptu:

Node: database
Node: tables
Node: table
Node: column
Node: column
Node: column
Node: table
Node: column
Node: column
Node: column
Node: column
Node: column
Node: column
Node: column
Node: column
Node: table
Node: column
Node: column
Node: column
Node: column
Poznámka: schválně jsem zvolil toto zdrojové XML, aby bylo patrné, že se při proudovém zpracování ztratí informace o pozici uzlu ve stromu.

13. Přečtení podrobnějších informací o uzlu

O právě zpracovávaném uzlu lze ve skutečnosti získat i mnohé další informace, zejména informace o jeho atributech. Opět se podívejme na jednoduchý skript, který vždy vypíše název uzlu a potom (odsazeně) i jména a hodnoty jeho atributů:

import xml.sax
 
 
class XmlHandler(xml.sax.ContentHandler):
    def startElement(self, name, attributes):
        print("Node:", name)
        for (k,v) in attributes.items():
            print("\t", k, v)
 
 
parser = xml.sax.make_parser()
parser.setContentHandler(XmlHandler())
 
with open("db.public.xml", "r") as fin:
    parser.parse(fin)

Výsledek by měl pro vstupní soubor db.public.xml vypadat takto:

Node: database
         name test1
         schema public
         type PostgreSQL - 9.6.10
Node: tables
Node: table
         name department
         numRows 0
         remarks
         schema public
         type TABLE
Node: column
         autoUpdated false
         defaultValue null
         digits 0
         id 0
         name id
         nullable false
         remarks
         size 10
         type int4
         typeCode 4
Node: column
         autoUpdated false
         defaultValue null
         digits 0
         id 1
         name name
         nullable false
         remarks
         size 20
         type varchar
         typeCode 12
Node: column
         autoUpdated false
         defaultValue null
         digits 0
         id 2
         name location
         nullable false
         remarks
         size 20
         type varchar
         typeCode 12
Node: table
         name employee
         numRows 0
         remarks
         schema public
         type TABLE
Node: column
         autoUpdated false
         defaultValue null
         digits 0
         id 0
         name id
         nullable false
         remarks
         size 10
         type int4
         typeCode 4
Node: column
         autoUpdated false
         defaultValue null
         digits 0
         id 1
         name name
         nullable false
         remarks
         size 20
         type varchar
         typeCode 12
Node: column
         autoUpdated false
         defaultValue null
         digits 0
         id 2
         name job
         nullable false
         remarks
         size 20
         type varchar
         typeCode 12
Node: column
         autoUpdated false
         defaultValue null
         digits 0
         id 3
         name manager
         nullable true
         remarks
         size 10
         type int4
         typeCode 4
Node: column
         autoUpdated false
         defaultValue null
         digits 0
         id 4
         name hiredate
         nullable false
         remarks
         size 13
         type date
         typeCode 91
Node: column
         autoUpdated false
         defaultValue null
         digits 0
         id 5
         name salary
         nullable false
         remarks
         size 10
         type int4
         typeCode 4
Node: column
         autoUpdated false
         defaultValue null
         digits 0
         id 6
         name comment
         nullable true
         remarks
         size 10
         type int4
         typeCode 4
Node: column
         autoUpdated false
         defaultValue null
         digits 0
         id 7
         name department
         nullable false
         remarks
         size 10
         type int4
         typeCode 4
Node: table
         name project
         numRows 0
         remarks
         schema public
         type TABLE
Node: column
         autoUpdated false
         defaultValue null
         digits 0
         id 0
         name id
         nullable false
         remarks
         size 10
         type int4
         typeCode 4
Node: column
         autoUpdated false
         defaultValue null
         digits 0
         id 1
         name employee
         nullable false
         remarks
         size 10
         type int4
         typeCode 4
Node: column
         autoUpdated false
         defaultValue null
         digits 0
         id 2
         name startdate
         nullable false
         remarks
         size 13
         type date
         typeCode 91
Node: column
         autoUpdated false
         defaultValue null
         digits 0
         id 3
         name enddate
         nullable false
         remarks
         size 13
         type date
         typeCode 91

14. Programová filtrace uzlů podle jejich typu

Relativně snadno lze odfiltrovat pouze uzly určitého typu, a to nezávisle na tom, kde se ve stromu reprezentovaném v XML souboru tyto uzly nachází – viz podtržená část skriptu (jedná se skutečně o ten nejprimitivnější způsob, mnohdy však plně dostačující):

import xml.sax
 
 
class XmlHandler(xml.sax.ContentHandler):
 
    def startElement(self, name, attributes):
        if name == "table":
            print("Node:", name)
            for (k,v) in attributes.items():
                print("\t", k, v)
 
 
parser = xml.sax.make_parser()
parser.setContentHandler(XmlHandler())
 
with open("db.public.xml", "r") as fin:
    parser.parse(fin)

Nyní bude výpis kratší, protože si zobrazíme pouze informace o tabulkách, nic dalšího (tedy nebudeme například zobrazovat podrobnější informace o jednotlivých sloupcích tabulky):

Node: table
         name department
         numRows 0
         remarks
         schema public
         type TABLE
Node: table
         name employee
         numRows 0
         remarks
         schema public
         type TABLE
Node: table
         name project
         numRows 0
         remarks
         schema public
         type TABLE

15. Zobrazení informací o poduzlech vybraných uzlů

Události jsou knihovnou xml.sax generovány nejenom ve chvíli, kdy parser načte začátek elementu, ale i tehdy, pokud je načten konec elementu. To nám umožňuje si vytvořit konečný automat použitelný například ve chvíli, kdy budeme chtít zobrazit obsah uzlů/elementů column, ovšem pouze takových elementů, které leží v uzlu table. Nejjednodušším (i když ne zcela korektním) řešením je realizace triviálního konečného automatu s jediným přechodem mezi stavy „zpracovávám tabulku“ a „nacházím se mimo tabulku“:

import xml.sax
 
 
class XmlHandler(xml.sax.ContentHandler):
 
    def __init__(self):
        self.in_table = False
 
    def startElement(self, name, attributes):
        if name == "table":
            print("Found table:", attributes["name"])
            self.in_table = True
 
        if self.in_table and name == "column":
            print("\tColumn:", attributes["name"])
            for (k,v) in attributes.items():
                print("\t\t", k, v)
 
    def endElement(self, name):
        if name == "table":
            self.in_table = False
 
 
parser = xml.sax.make_parser()
parser.setContentHandler(XmlHandler())
 
with open("db.public.xml", "r") as fin:
    parser.parse(fin)

Výsledky budou vypadat takto:

Found table: department
        Column: id
                 autoUpdated false
                 defaultValue null
                 digits 0
                 id 0
                 name id
                 nullable false
                 remarks
                 size 10
                 type int4
                 typeCode 4
        Column: name
                 autoUpdated false
                 defaultValue null
                 digits 0
                 id 1
                 name name
                 nullable false
                 remarks
                 size 20
                 type varchar
                 typeCode 12
        Column: location
                 autoUpdated false
                 defaultValue null
                 digits 0
                 id 2
                 name location
                 nullable false
                 remarks
                 size 20
                 type varchar
                 typeCode 12
Found table: employee
        Column: id
                 autoUpdated false
                 defaultValue null
                 digits 0
                 id 0
                 name id
                 nullable false
                 remarks
                 size 10
                 type int4
                 typeCode 4
        Column: name
                 autoUpdated false
                 defaultValue null
                 digits 0
                 id 1
                 name name
                 nullable false
                 remarks
                 size 20
                 type varchar
                 typeCode 12
        Column: job
                 autoUpdated false
                 defaultValue null
                 digits 0
                 id 2
                 name job
                 nullable false
                 remarks
                 size 20
                 type varchar
                 typeCode 12
        Column: manager
                 autoUpdated false
                 defaultValue null
                 digits 0
                 id 3
                 name manager
                 nullable true
                 remarks
                 size 10
                 type int4
                 typeCode 4
        Column: hiredate
                 autoUpdated false
                 defaultValue null
                 digits 0
                 id 4
                 name hiredate
                 nullable false
                 remarks
                 size 13
                 type date
                 typeCode 91
        Column: salary
                 autoUpdated false
                 defaultValue null
                 digits 0
                 id 5
                 name salary
                 nullable false
                 remarks
                 size 10
                 type int4
                 typeCode 4
        Column: comment
                 autoUpdated false
                 defaultValue null
                 digits 0
                 id 6
                 name comment
                 nullable true
                 remarks
                 size 10
                 type int4
                 typeCode 4
        Column: department
                 autoUpdated false
                 defaultValue null
                 digits 0
                 id 7
                 name department
                 nullable false
                 remarks
                 size 10
                 type int4
                 typeCode 4
Found table: project
        Column: id
                 autoUpdated false
                 defaultValue null
                 digits 0
                 id 0
                 name id
                 nullable false
                 remarks
                 size 10
                 type int4
                 typeCode 4
        Column: employee
                 autoUpdated false
                 defaultValue null
                 digits 0
                 id 1
                 name employee
                 nullable false
                 remarks
                 size 10
                 type int4
                 typeCode 4
        Column: startdate
                 autoUpdated false
                 defaultValue null
                 digits 0
                 id 2
                 name startdate
                 nullable false
                 remarks
                 size 13
                 type date
                 typeCode 91
        Column: enddate
                 autoUpdated false
                 defaultValue null
                 digits 0
                 id 3
                 name enddate
                 nullable false
                 remarks
                 size 13
                 type date
                 typeCode 91

16. Chování knihovny xml.sax v případě nevalidního XML

Vzhledem k tomu, že SAX parser zpracovává data uložená v XML postupně, nedokáže správně zareagovat na nevalidní XML. Resp. přesněji řečeno: parser dokáže detekovat a nahlásit chybu, ovšem většinou až ve chvíli, kdy jsou předchozí uzly již aplikací zpracovány, takže samotná aplikace se musí nějakým způsobem postarat o obnovení původního stavu (před načtením XML). Můžeme si to ukázat na jednoduchém příkladu – v souboru pom.xml schválně změníme jméno uzavíracího tagu tak, že vznikne nevalidní XML (podtržená část):

bitcoin_skoleni

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.tisnik.uberproject.test</groupId>
  <artifactId>test-app-junit-dependency</artifactId>
  <version>1.0</version>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
    </dependency>
    <dependency>
      <groupId>foo</groupId>
      <artifactId>foo</artifactId>
      <version>1.0.0</version>
    </dependency>
    <dependency>
      <groupId>bar</groupId>
      <artifactId>bar</artifactId>
      <version>1.2.3</version>
    </dependency>
  </dependencies>
</projXYZect>

Průběh zpracování může vypadat takto:

Node: project
Node: modelVersion
Node: groupId
Node: artifactId
Node: version
Node: dependencies
Node: dependency
Node: groupId
Node: artifactId
Node: version
Node: dependency
Node: groupId
Node: artifactId
Node: version
Node: dependency
Node: groupId
Node: artifactId
Node: version
Traceback (most recent call last):
  File "/usr/lib/python3.8/xml/sax/expatreader.py", line 217, in feed
    self._parser.Parse(data, isFinal)
xml.parsers.expat.ExpatError: mismatched tag: line 23, column 2
 
During handling of the above exception, another exception occurred:
 
Traceback (most recent call last):
  File "sax4.py", line 15, in <module>
    parser.parse(fin)
  File "/usr/lib/python3.8/xml/sax/expatreader.py", line 111, in parse
    xmlreader.IncrementalParser.parse(self, source)
  File "/usr/lib/python3.8/xml/sax/xmlreader.py", line 125, in parse
    self.feed(buffer)
  File "/usr/lib/python3.8/xml/sax/expatreader.py", line 221, in feed
    self._err_handler.fatalError(exc)
  File "/usr/lib/python3.8/xml/sax/handler.py", line 38, in fatalError
    raise exception
xml.sax._exceptions.SAXParseException: pom2.xml:23:2: mismatched tag

V případě, že má příslušná aplikace dostatečné množství strojového času, může být vhodnější provést nejdříve validaci a teprve poté pokus o načtení XML.

17. Odkazy na Internetu

  1. lxml – XML and HTML with Python
    https://lxml.de/index.html
  2. lxml FAQ – Frequently Asked Questions
    https://lxml.de/FAQ.html
  3. Knihovna lxml na PyPi
    https://pypi.org/project/lxml/
  4. ElementTree and lxml
    https://wiki.python.org/mo­in/Tutorials%20on%20XML%20pro­cessing%20with%20Python
  5. ElementTree Overview
    http://effbot.org/zone/element-index.htm
  6. Elements and Element Trees
    http://effbot.org/zone/element.htm
  7. Python XML processing with lxml
    http://infohost.nmt.edu/tcc/hel­p/pubs/pylxml/web/index.html
  8. Dive into Python 3: XML
    http://www.diveintopython3­.net/xml.html
  9. XML Tutorial na zvon.org
    http://www.zvon.org/comp/r/tut-XML.html
  10. Extensible Markup Language (XML) 1.0 (Fifth Edition)
    https://www.w3.org/TR/REC-xml/
  11. XML Processing Modules (pro Python)
    https://docs.python.org/3/li­brary/xml.html
  12. Extensible Markup Language
    https://cs.wikipedia.org/wi­ki/Extensible_Markup_Langu­age
  13. Slabikář XML – odkazy
    https://www.interval.cz/clan­ky/slabikar-xml-odkazy/
  14. XML editors
    http://www.xml-dev.com/
  15. lxml FAQ – Frequently Asked Questions
    https://lxml.de/FAQ.html
  16. XML pro začátečníky – 1. část
    http://programujte.com/cla­nek/2007030501-xml-pro-zacatecniky-1-cast/
  17. XML pro web aneb od teorie k praxi, 2.díl
    https://www.zive.cz/clanky/xml-pro-web-aneb-od-teorie-k-praxi-2dil/sc-3-a-109709/default.aspx
  18. XML Schema
    https://cs.wikipedia.org/wi­ki/XML_Schema
  19. Meaning of – <?xml version=“1.0” encoding=“utf-8”?>
    https://stackoverflow.com/qu­estions/13743250/meaning-of-xml-version-1–0-encoding-utf-8#27398439
  20. Beautiful Soup
    https://www.crummy.com/sof­tware/BeautifulSoup/
  21. Web scraping
    https://en.wikipedia.org/wi­ki/Web_scraping
  22. XPath examples
    https://www.w3schools.com/xml/xpat­h_examples.asp
  23. XPath Axes
    https://www.w3schools.com/xml/xpat­h_axes.asp
  24. Guide to naming conventions on groupId, artifactId, and version
    http://maven.apache.org/gu­ides/mini/guide-naming-conventions.html
  25. What is meaning of .// in XPath?
    https://stackoverflow.com/qu­estions/31375091/what-is-meaning-of-in-xpath
  26. Using „//“ And „.//“ Expressions In XPath XML
    https://www.bennadel.com/blog/2142-using-and-expressions-in-xpath-xml-search-directives-in-coldfusion.htm
  27. Using parent dot notation in xpath to find another branch in the XML tree
    https://stackoverflow.com/qu­estions/5370544/using-parent-dot-notation-in-xpath-to-find-another-branch-in-the-xml-tree#5370817
  28. xml.sax — Support for SAX2 parsers
    https://docs.python.org/3/li­brary/xml.sax.html
  29. Well Formed XML
    https://www.w3resource.com/xml/well-formed.php
  30. XML Tutorial
    https://www.w3resource.com/xml/xml­.php
  31. Why use JSON over XML?
    https://www.sitepoint.com/json-vs-xml/
  32. XML and XPath
    https://www.w3schools.com/XML/xml_xpat­h.asp
  33. XPath (Wikipedia)
    https://en.wikipedia.org/wiki/XPath
  34. RFC7159
    https://www.ietf.org/rfc/rfc7159.txt
  35. Python – XML Processing
    https://www.tutorialspoin­t.com/python/python_xml_pro­cessing.htm
  36. How to Process XML in Python – Element Tree Library
    https://www.javatpoint.com/how-to-process-xml-in-python
  37. XML Frequently Asked Questions
    http://www.hwg.org/resources/faq­s/xmlFAQ.html
  38. OrderedDict
    https://docs.python.org/3/li­brary/collections.html#co­llections.OrderedDict

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.