XML a JSON v Pythonu

Python má XML ve standardní knihovně: modul xml.etree.ElementTree zvládne většinu běžné práce. Pro plné XPath, validaci proti XSD a XSLT se používá knihovna lxml a na obří soubory iterparse. Kapitola ukazuje, kdy co použít, jak hledat v datech, pracovat se jmennými prostory, zachytit chyby a XML vytvořit.

Autor: Aktualizováno

Čtení přes ElementTree

Python
import xml.etree.ElementTree as ET

strom = ET.parse('objednavka.xml')           # nebo ET.fromstring(text) — vrací rovnou kořen
koren = strom.getroot()

print(koren.get('cislo'))                     # atribut: 2026-117
print(koren.findtext('zakaznik'))             # text prvku: Jana Nováková

for polozka in koren.iter('polozka'):         # iter() hledá v celém stromu, findall() jen v cestě
    mnozstvi = int(polozka.get('mnozstvi'))
    print(f'{polozka.text} × {mnozstvi}')

# Hodnoty jsou vždy řetězce, čísla převeď sám
celkem = sum(float(p.get('cena')) * int(p.get('mnozstvi')) for p in koren.findall('polozka'))

Prvek (Element) se chová jako seznam svých potomků: len(prvek), prvek[0] i for dite in prvek fungují. Text za koncovou značkou potomka je v jeho atributu tail, ne v text rodiče.

Pozor na pravdivostní hodnotu: prvek bez potomků je v podmínce nepravdivý, i když existuje (Python 3.12 na to upozorňuje varováním). Místo if koren.find('zakaznik'): piš if koren.find('zakaznik') is not None:.

ElementTree, lxml, nebo iterparse?

Všechny ukázky pracují s touto objednávkou:

objednavka.xml
<?xml version="1.0" encoding="UTF-8"?>
<objednavka cislo="2026-117">
  <zakaznik>Jana Nováková</zakaznik>
  <polozka kod="HR-01" mnozstvi="2" cena="149.90">Hrnek</polozka>
  <polozka kod="TA-07" mnozstvi="1" cena="219.00">Talíř</polozka>
</objednavka>
xml.etree.ElementTreelxmliterparsexml.dom.minidom
Instalacesoučást Pythonupip install lxmlv ElementTree i lxmlsoučást Pythonu
Paměťcelý dokumentcelý dokumentjen rozpracovaný záznamcelý dokument, nejvíc
XPathjen podmnožina v find()plné XPath 1.0po záznamech přes find()ne
Validace XSD, XSLTneano—ne
Kdy použítběžné čtení a zápis bez závislostídotazy, validace, transformace, čísla řádkůsoubory ve stovkách MB, feedystarší kód, rychlé toprettyxml()

API knihovny lxml je s ElementTree kompatibilní: find(), findall(), get(), text i iter() fungují stejně. Kód napsaný pro ElementTree proto obvykle stačí přepnout změnou importu a pak využít, co lxml přidává navíc.

Chyby parsování

Python
import xml.etree.ElementTree as ET

try:
    koren = ET.fromstring('<objednavka><polozka></objednavka>')
except ET.ParseError as e:
    radek, sloupec = e.position
    print(f'Řádek {radek}, sloupec {sloupec}: {e}')
    # Řádek 1, sloupec 23: mismatched tag: line 1, column 23

# lxml hlásí stejné chyby jako libxml2 (PHP, xmllint)
from lxml import etree
try:
    etree.fromstring(b'<objednavka><polozka></objednavka>')
except etree.XMLSyntaxError as e:
    print(e.lineno, e.msg)
    # 1 Opening and ending tag mismatch: polozka line 1 and objednavka, line 1, column 35

ElementTree používá parser expat, a proto hlásí krátké zprávy jako not well-formed (invalid token) nebo mismatched tag. Co znamenají a jak je opravit, najdeš v přehledu chybových hlášek, kde jsou zprávy Pythonu u každé příčiny.

Kódování a lxml

Python
text = open('feed.xml', encoding='utf-8').read()
etree.fromstring(text)
# ValueError: Unicode strings with encoding declaration are not supported.

# Řešení: předat bajty a nechat parser přečíst kódování z deklarace
with open('feed.xml', 'rb') as f:
    koren = etree.fromstring(f.read())    # nebo rovnou etree.parse('feed.xml')

Soubor s XML proto otevírej binárně ('rb') a dekódování nech na parseru. Parser se řídí deklarací encoding, takže správně načte i feed ve windows-1250.

Vytvoření XML

Python
import xml.etree.ElementTree as ET

koren = ET.Element('objednavka', cislo='2026-118')
ET.SubElement(koren, 'zakaznik').text = 'Petr & syn'         # & se zapíše jako &amp;
polozka = ET.SubElement(koren, 'polozka', kod='MI-03', mnozstvi='3')
polozka.text = 'Miska'

strom = ET.ElementTree(koren)
ET.indent(strom)                                            # odsazení (Python 3.9+)
strom.write('export.xml', encoding='utf-8', xml_declaration=True)

ET.tostring(koren, encoding='unicode')                      # jako str; bez encoding vrací bytes

Řetězce neskládej ručně (f'<a>{hodnota}</a>') — jeden znak & nebo < v datech udělá z výsledku nevalidní XML. Knihovna speciální znaky escapuje sama. Hotový feed nebo export si pak můžeš zkontrolovat ve validátoru.

Serializace XML

Při převodu XML stromu na text (serializaci) knihovna zapíše zvláštní znaky podle pravidel XML, například ampersand jako &amp;. Zápis zpět nemusí zachovat původní prefixy, odsazení, CDATA nebo pořadí atributů; testuj obsah a požadované kódování.

text = ET.tostring(koren, encoding="unicode")
ET.ElementTree(koren).write("vystup.xml", encoding="utf-8", xml_declaration=True)

DOM

ElementTree je stromové API, ale neimplementuje celý W3C DOM. Pokud potřebuješ DOM uzly, standardní knihovna nabízí xml.dom.minidom. Pro práci s velkými soubory použij iterparse a průběžně uvolňuj dokončené elementy.

Velké soubory: iterparse

Python
import xml.etree.ElementTree as ET

# Feed se 100 000 produkty po jednom záznamu, bez načtení celého do paměti
kontext = ET.iterparse('feed.xml', events=('start', 'end'))
_, koren = next(kontext)                      # první událost je začátek kořene

for udalost, prvek in kontext:
    if udalost == 'end' and prvek.tag == 'SHOPITEM':
        uloz(prvek.findtext('PRODUCTNAME'), float(prvek.findtext('PRICE_VAT')))
        koren.clear()                         # zahodit zpracované záznamy

Samotné prvek.clear() nestačí: prázdné prvky zůstávají zavěšené na kořeni a paměť u milionu záznamů stejně roste. Proto se na začátku vezme kořen a po každém záznamu se vyčistí celý. V lxml se totéž dělá odebráním už zpracovaných sourozenců:

Python
# lxml umí filtrovat rovnou podle názvu (i s jmenným prostorem)
for _, prvek in etree.iterparse('zbozi.xml', tag='{http://www.zbozi.cz/ns/offer/1.0}SHOPITEM'):
    zpracuj(prvek)
    prvek.clear(keep_tail=True)
    while prvek.getprevious() is not None:    # uvolnit i dřív zpracované sourozence
        del prvek.getparent()[0]

Hledání: cesty v ElementTree

Python
koren.find('polozka')                         # první přímý potomek <polozka>, jinak None
koren.findall('.//polozka[@kod="TA-07"]')     # podle hodnoty atributu
koren.findall('polozka[1]')                   # podle pozice (od 1)
koren.findtext('neexistuje', default='')      # bez default vrací None

# Tohle ElementTree neumí — jen omezenou podmnožinu XPath:
# koren.findall('polozka[@mnozstvi > 1]')     # SyntaxError: invalid predicate

find() a findall() berou jen omezenou podmnožinu XPath: názvy, *, ., .., //, test na atribut, na text potomka a na pozici. Porovnání čísel, funkce jako contains() nebo osy v ní nejsou — pro ně sáhni po lxml.

Plné XPath v lxml

Python
from lxml import etree

strom = etree.parse('objednavka.xml')

# Plné XPath 1.0: porovnání, funkce, výsledkem může být i číslo nebo text
kody = strom.xpath('//polozka[@mnozstvi > 1]/@kod')        # ['HR-01']
pocet = strom.xpath('count(//polozka)')                    # 2.0 (float)
jmeno = strom.xpath('string(/objednavka/zakaznik)')        # 'Jana Nováková'

# Proměnné místo skládání řetězců (ochrana před XPath injection)
polozka = strom.xpath('//polozka[@kod = $kod]', kod='HR-01')[0]
print(polozka.sourceline)                                  # číslo řádku v souboru: 4

lxml stojí na knihovně libxml2 a podporuje XPath 1.0. Funkce z novějších verzí (matches(), string-join()) v něm nejsou; regulární výrazy nabízí přes rozšíření EXSLT. Výraz si před vložením do kódu vyzkoušej v testeru s volbou XPath 1.0.

Jmenné prostory

Python
text = '<f:faktura xmlns:f="http://example.cz/faktura"><f:cislo>FV2026-042</f:cislo></f:faktura>'
NS = {'fa': 'http://example.cz/faktura'}      # prefix v dotazu nemusí být stejný jako v dokumentu

# ElementTree: slovník prefixů, nebo zápis {uri}jméno
koren = ET.fromstring(text)
koren.findtext('fa:cislo', namespaces=NS)                   # 'FV2026-042'
koren.findtext('{http://example.cz/faktura}cislo')
koren.tag                                                   # '{http://example.cz/faktura}faktura'

# lxml: stejný slovník v xpath()
etree.fromstring(text.encode()).xpath('//fa:cislo/text()', namespaces=NS)

# Výchozí jmenný prostor (xmlns="…" bez prefixu) — ElementTree zná klíč '' pro find*()
koren.findall('polozka', namespaces={'': 'http://example.cz/objednavka'})

Nejčastější chyba: dokument má výchozí jmenný prostor a findall('polozka') nic nenajde, protože se prvek ve skutečnosti jmenuje {http://example.cz/objednavka}polozka. V lxml xpath() klíč '' nefunguje — prefix je potřeba vymyslet a psát //o:polozka. Víc v kapitole Jmenné prostory.

Validace proti XSD a XSLT

Standardní knihovna validaci neumí, lxml ano. Chyby mají stejné znění jako xmllint, takže je najdeš i v přehledu chyb validace.

Python
from lxml import etree

schema = etree.XMLSchema(etree.parse('objednavka.xsd'))
dokument = etree.parse('objednavka.xml')

if not schema.validate(dokument):
    for chyba in schema.error_log:
        print(f'Řádek {chyba.line}: {chyba.message}')

# nebo schema.assertValid(dokument), které vyhodí DocumentInvalid

Stejně jednoduše lxml spustí transformaci XSLT 1.0:

Python
transformace = etree.XSLT(etree.parse('faktura.xsl'))
html = transformace(etree.parse('faktura.xml'))
html.write_output('faktura.html')                # respektuje xsl:output

Bezpečnost

ElementTree externí entity nenačítá a s aktuální verzí expat odolá i entitním bombám. lxml od verze 5 rozbaluje jen interní entity, ale načítá DTD a u starších verzí rozbaluje i externí. Pro XML z cizích zdrojů proto použij knihovnu defusedxml se stejným API jako standardní moduly, nebo lxml s parserem etree.XMLParser(resolve_entities=False, no_network=True). Proč na tom záleží, vysvětluje kapitola o útoku XXE.

Načtení JSON

JSON parser vrací datovou strukturu, ne XML strom. Chyby syntaxe zpracuj jako chyby vstupu; data ověř také strukturálně.

import json
data = json.loads('{"jmeno":"Jana","vek":20}')
print(data["jmeno"])

Vytvoření JSON

Vytvoř hodnotu v paměti a nech její textový zápis na knihovně.

data = {"jmeno": "Jana", "vek": 20, "aktivni": True}

Serializace JSON

Neskládej text ručně. Ověř přesnost čísel, reprezentaci dat a chování nečíselných hodnot.

text = json.dumps(data, ensure_ascii=False, allow_nan=False, indent=2)

JSON Schema

Standardní knihovna neposkytuje plnou validaci JSON Schema. Vyber knihovnu s podporou požadované verze pravidel JSON Schema z oficiální dokumentace či katalogu. Základy a rozdíly verzí vysvětluje kurz JSON Schema.

Ukázka vyžaduje instalaci uvedeného balíčku. Kontrola format závisí na nastavení knihovny; validace nenahrazuje obchodní pravidla.

from jsonschema import Draft202012Validator
schema = {"type": "object", "properties": {"vek": {"type": "integer"}}, "required": ["vek"]}
Draft202012Validator.check_schema(schema)
Draft202012Validator(schema).validate({"vek": 20})

Dokumentace API

Příklady JSON vycházejí z oficiální dokumentace jazyka. Překlady typů, maximální hloubka a chování při chybě jsou vlastnosti knihovny, ne samotné syntaxe JSON.