Obsah
1. Čtyři způsoby zpracování XML v×Pythonu
2. Testovací data ve formátu XML
4. Příklad použití knihovny lxml
5. Přístup k atributům a poduzlům naparsovaného dokumentu
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
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
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(ů):
- 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í.
- 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ů.
- 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.
- 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.
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>
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)
$ 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')]))]))]))])
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:
- Jméno (první prvek dvojice) začíná znakem zavináče
- 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:
- Jméno (první prvek dvojice) nezačíná znakem zavináče
- 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" } ] } } }
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
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
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):
<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
- lxml – XML and HTML with Python
https://lxml.de/index.html - lxml FAQ – Frequently Asked Questions
https://lxml.de/FAQ.html - Knihovna lxml na PyPi
https://pypi.org/project/lxml/ - ElementTree and lxml
https://wiki.python.org/moin/Tutorials%20on%20XML%20processing%20with%20Python - ElementTree Overview
http://effbot.org/zone/element-index.htm - Elements and Element Trees
http://effbot.org/zone/element.htm - Python XML processing with lxml
http://infohost.nmt.edu/tcc/help/pubs/pylxml/web/index.html - Dive into Python 3: XML
http://www.diveintopython3.net/xml.html - XML Tutorial na zvon.org
http://www.zvon.org/comp/r/tut-XML.html - Extensible Markup Language (XML) 1.0 (Fifth Edition)
https://www.w3.org/TR/REC-xml/ - XML Processing Modules (pro Python)
https://docs.python.org/3/library/xml.html - Extensible Markup Language
https://cs.wikipedia.org/wiki/Extensible_Markup_Language - Slabikář XML – odkazy
https://www.interval.cz/clanky/slabikar-xml-odkazy/ - XML editors
http://www.xml-dev.com/ - lxml FAQ – Frequently Asked Questions
https://lxml.de/FAQ.html - XML pro začátečníky – 1. část
http://programujte.com/clanek/2007030501-xml-pro-zacatecniky-1-cast/ - 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 - XML Schema
https://cs.wikipedia.org/wiki/XML_Schema - Meaning of – <?xml version=“1.0” encoding=“utf-8”?>
https://stackoverflow.com/questions/13743250/meaning-of-xml-version-1–0-encoding-utf-8#27398439 - Beautiful Soup
https://www.crummy.com/software/BeautifulSoup/ - Web scraping
https://en.wikipedia.org/wiki/Web_scraping - XPath examples
https://www.w3schools.com/xml/xpath_examples.asp - XPath Axes
https://www.w3schools.com/xml/xpath_axes.asp - Guide to naming conventions on groupId, artifactId, and version
http://maven.apache.org/guides/mini/guide-naming-conventions.html - What is meaning of .// in XPath?
https://stackoverflow.com/questions/31375091/what-is-meaning-of-in-xpath - Using „//“ And „.//“ Expressions In XPath XML
https://www.bennadel.com/blog/2142-using-and-expressions-in-xpath-xml-search-directives-in-coldfusion.htm - Using parent dot notation in xpath to find another branch in the XML tree
https://stackoverflow.com/questions/5370544/using-parent-dot-notation-in-xpath-to-find-another-branch-in-the-xml-tree#5370817 - xml.sax — Support for SAX2 parsers
https://docs.python.org/3/library/xml.sax.html - Well Formed XML
https://www.w3resource.com/xml/well-formed.php - XML Tutorial
https://www.w3resource.com/xml/xml.php - Why use JSON over XML?
https://www.sitepoint.com/json-vs-xml/ - XML and XPath
https://www.w3schools.com/XML/xml_xpath.asp - XPath (Wikipedia)
https://en.wikipedia.org/wiki/XPath - RFC7159
https://www.ietf.org/rfc/rfc7159.txt - Python – XML Processing
https://www.tutorialspoint.com/python/python_xml_processing.htm - How to Process XML in Python – Element Tree Library
https://www.javatpoint.com/how-to-process-xml-in-python - XML Frequently Asked Questions
http://www.hwg.org/resources/faqs/xmlFAQ.html - OrderedDict
https://docs.python.org/3/library/collections.html#collections.OrderedDict