Nacházíte se zde: Domů ‣ Ponořme se do Pythonu 3 ‣
Úroveň obtížnosti: ♦♦♦♦♢
❝ In the archonship of Aristaechmus, Draco enacted his ordinances. ❞
(Za vlády Aristaechma uzákonil Drakon svá pravidla.)
— Aristoteles
Téměř všechny kapitoly této knihy se točí kolem příkladů kódu. XML nesouvisí s kódem, ale s daty. Jedním z míst, kde se XML běžně používá, je „publikovaný obsah“ (syndication feeds), ve kterém se udržuje seznam posledních článků blogu, fóra nebo jiného, často aktualizovaného obsahu webového místa. Nejpopulárnější blogovací programy vytvářejí obsah (feed), a kdykoliv je publikován nový článek, diskusní vlákno nebo zpráva na blogu, tento obsah aktualizují. Blog můžeme sledovat tak, že se „přihlásíme k odběru“ jeho obsahu (feed). Více blogů můžeme sledovat tak, že použijeme k tomu určený „nástroj pro sdružování obsahu (feed aggregator)“, jako je například Google Reader.
V této kapitole budeme pracovat s následujícími XML daty. Jde o publikovaný obsah (feed) — konkrétně o Atom syndication feed.
<?xml version='1.0' encoding='utf-8'?>
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
<title>dive into mark</title>
<subtitle>currently between addictions</subtitle>
<id>tag:diveintomark.org,2001-07-29:/</id>
<updated>2009-03-27T21:56:07Z</updated>
<link rel='alternate' type='text/html' href='http://diveintomark.org/'/>
<link rel='self' type='application/atom+xml' href='http://diveintomark.org/feed/'/>
<entry>
<author>
<name>Mark</name>
<uri>http://diveintomark.org/</uri>
</author>
<title>Dive into history, 2009 edition</title>
<link rel='alternate' type='text/html'
href='http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition'/>
<id>tag:diveintomark.org,2009-03-27:/archives/20090327172042</id>
<updated>2009-03-27T21:56:07Z</updated>
<published>2009-03-27T17:20:42Z</published>
<category scheme='http://diveintomark.org' term='diveintopython'/>
<category scheme='http://diveintomark.org' term='docbook'/>
<category scheme='http://diveintomark.org' term='html'/>
<summary type='html'>Putting an entire chapter on one page sounds
bloated, but consider this &mdash; my longest chapter so far
would be 75 printed pages, and it loads in under 5 seconds&hellip;
On dialup.</summary>
</entry>
<entry>
<author>
<name>Mark</name>
<uri>http://diveintomark.org/</uri>
</author>
<title>Accessibility is a harsh mistress</title>
<link rel='alternate' type='text/html'
href='http://diveintomark.org/archives/2009/03/21/accessibility-is-a-harsh-mistress'/>
<id>tag:diveintomark.org,2009-03-21:/archives/20090321200928</id>
<updated>2009-03-22T01:05:37Z</updated>
<published>2009-03-21T20:09:28Z</published>
<category scheme='http://diveintomark.org' term='accessibility'/>
<summary type='html'>The accessibility orthodoxy does not permit people to
question the value of features that are rarely useful and rarely used.</summary>
</entry>
<entry>
<author>
<name>Mark</name>
</author>
<title>A gentle introduction to video encoding, part 1: container formats</title>
<link rel='alternate' type='text/html'
href='http://diveintomark.org/archives/2008/12/18/give-part-1-container-formats'/>
<id>tag:diveintomark.org,2008-12-18:/archives/20081218155422</id>
<updated>2009-01-11T19:39:22Z</updated>
<published>2008-12-18T15:54:22Z</published>
<category scheme='http://diveintomark.org' term='asf'/>
<category scheme='http://diveintomark.org' term='avi'/>
<category scheme='http://diveintomark.org' term='encoding'/>
<category scheme='http://diveintomark.org' term='flv'/>
<category scheme='http://diveintomark.org' term='GIVE'/>
<category scheme='http://diveintomark.org' term='mp4'/>
<category scheme='http://diveintomark.org' term='ogg'/>
<category scheme='http://diveintomark.org' term='video'/>
<summary type='html'>These notes will eventually become part of a
tech talk on video encoding.</summary>
</entry>
</feed>
⁂
Pokud už o XML něco víte, můžete tuto podkapitolu přeskočit.
XML představuje zobecněný způsob popisu hierarchických strukturovaných dat. XML-dokument obsahuje jeden nebo více elementů, které jsou ohraničeny počátečními a koncovými značkami (tag). Tohle je kompletní (i když poněkud nudný) XML dokument:
<foo> ①
</foo> ②
foo
.
foo
. Každá počáteční značka musí být uzavřena (spárována s) odpovídající koncovou značkou stejně, jako musíme párovat závorky v matematice nebo v textu.
Elementy lze zanořovat do libovolné hloubky. O elementu bar
uvnitř elementu foo
se říká, že je subelementem nebo potomkem (child) elementu foo
.
<foo>
<bar></bar>
</foo>
Prvnímu elementu v každém XML dokumentu se říká kořenový element (root element). XML dokument může mít jen jeden kořenový element. Následující text není XML dokumentem, protože obsahuje dva kořenové elementy:
<foo></foo>
<bar></bar>
Elementy mohou nést atributy, což jsou dvojice jméno-hodnota. Atributy se uvádějí uvnitř počáteční značky elementu a oddělují se bílými znaky. Uvnitř jednoho elementu se jména atributů nesmějí opakovat. Hodnoty atributů musí být uzavřeny v uvozovkách nebo v apostrofech.
<foo lang='en'> ①
<bar id='papayawhip' lang="fr"></bar> ②
</foo>
foo
má jeden atribut pojmenovaný lang
. Hodnotou jeho atributu lang
je en
.
bar
má dva atributy pojmenované id
a lang
. Jeho atribut lang
má hodnotu fr
. Nedochází vůbec k žádnému konfliktu s elementem foo
. Každý element má svou vlastní sadu atributů.
Pokud je v jednom elementu uvedeno víc atributů, pak jejich pořadí není významné. Atributy elementu tvoří neuspořádanou množinu dvojic klíčů a hodnot — jako pythonovský slovník. Počet atributů, které můžeme u každého elementu definovat, není nijak omezen.
Elementy mohou obsahovat text.
<foo lang='en'>
<bar lang='fr'>PapayaWhip</bar>
</foo>
Elementy, které neobsahují žádný text a nemají žádné potomky, jsou prázdné.
<foo></foo>
Prázdné elementy můžeme zapisovat zkráceně. Když do počáteční značky umístíme znak /
, můžeme koncovou značku úplně vynechat. XML dokument z předchozího příkladu můžeme zkráceně zapsat takto:
<foo/>
Podobně jako můžeme pythonovské funkce deklarovat v různých modulech, XML elementy můžeme deklarovat v různých prostorech jmen. Prostory jmen se obvykle podobají zápisu URL. Výchozí prostor jmen definujeme pomocí deklarace xmlns
. Deklarace prostoru jmen vypadá podobně jako zápis atributu, ale plní odlišný účel.
<feed xmlns='http://www.w3.org/2005/Atom'> ①
<title>dive into mark</title> ②
</feed>
feed
se nachází v prostoru jmen http://www.w3.org/2005/Atom
.
title
se také nachází v prostoru jmen http://www.w3.org/2005/Atom
. Deklarace prostoru jmen ovlivní element, ve kterém se deklarace nachází, a dále všechny jeho dětské elementy (potomky).
Při deklaraci prostoru jmen můžeme použít také zápis xmlns:prefix
, čímž prostor jmen spřáhneme se zadaným prefixem. V takovém případě musí být každý element tohoto prostoru jmen explicitně deklarován se stejným prefixem.
<atom:feed xmlns:atom='http://www.w3.org/2005/Atom'> ①
<atom:title>dive into mark</atom:title> ②
</atom:feed>
feed
se nachází v prostoru jmen http://www.w3.org/2005/Atom
.
title
se také nachází v prostoru jmen http://www.w3.org/2005/Atom
.
Z pohledu syntaktického analyzátoru pro XML jsou přecházející dva XML dokumenty identické. Prostor jmen + jméno elementu = XML identita. Prefixy se používají pouze k odkazu na prostor jmen. To znamená, že konkrétní jméno prefixu (atom:
) je nepodstatné. Prostory jmen pasují, jména elementů se shodují, atributy (nebo neuvedení atributů) sedí, textový obsah každého elementu se také shoduje. To znamená, že se jedná o stejné XML dokumenty.
Na závěr uveďme, že XML dokumenty mohou na prvním řádku, před kořenovým elementem, uvádět informaci o znakovém kódování. (Pokud vás zajímá, jak může dokument obsahovat informaci, která musí být známa předtím, než se dokument zpracovává, pak detaily řešení této Hlavy XXII hledejte v sekci F specifikace XML (anglicky).)
<?xml version='1.0' encoding='utf-8'?>
Tak a teď už o XML víte dost na to, abyste mohli být nebezpeční!
⁂
Vezměme si nějaký weblog nebo v podstatě libovolný webový server s často aktualizovaným obsahem, jako je například CNN.com. Server má svůj nadpis („CNN.com“), podnadpis („Breaking News, U.S., World, Weather, Entertainment & Video News“), datum poslední aktualizace („updated 12:43 p.m. EDT, Sat May 16, 2009“) a seznam článků zveřejněných v různých časech. Každý článek má také nadpis, datum prvního zveřejnění (a možná také datum poslední aktualizace, pokud zveřejnili upřesnění nebo opravili překlep) a jedinečné URL.
The Atom syndication format je navržen tak, aby všechny tyto informace zachytil ve standardním tvaru. Můj weblog a CNN.com se sice velmi liší v návrhu, rozsahu a v návštěvnosti, ale oba mají stejnou základní strukturu. CNN.com má nadpis, můj blog má nadpis. CNN.com zveřejňuje články, já zveřejňuji články.
Na nejvyšší úrovni se nachází kořenový element, který používají všechny „Atom feed“ — element feed
v prostoru jmen http://www.w3.org/2005/Atom
.
<feed xmlns='http://www.w3.org/2005/Atom' ①
xml:lang='en'> ②
http://www.w3.org/2005/Atom
je prostor jmen pro Atom.
xml:lang
, který deklaruje jazyk elementu a jeho potomků. V tomto případě je atribut xml:lang
deklarován jen jednou, v kořenovém elementu. To znamená, že celý obsah (feed) je v angličtině.
Atom feed (chápejte tento název jako pojem) obsahuje pár informací i o dokumentu samotném (tedy o sobě). Jsou deklarovány jako potomci kořenového elementu feed
.
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
<title>dive into mark</title> ①
<subtitle>currently between addictions</subtitle> ②
<id>tag:diveintomark.org,2001-07-29:/</id> ③
<updated>2009-03-27T21:56:07Z</updated> ④
<link rel='alternate' type='text/html' href='http://diveintomark.org/'/> ⑤
dive into mark
.
currently between addictions
.
link
nemá žádný textový obsah, ale má tři atributy: rel
, type
a href
. Hodnota atributu rel
říká, jakého druhu odkaz je. Hodnota rel='alternate'
vyjadřuje, že jde o odkaz na alternativní reprezentaci tohoto obsahu (feed). Atribut type='text/html'
říká, že jde o odkaz na HTML stránku. Cíl odkazu je uveden v atributu href
.
Teď už víme, že jde o obsah (feed) pro místo zvané „dive into mark“, které se nachází na http://diveintomark.org/
a bylo naposledy aktualizováno 27. března 2009.
☞Ačkoliv v některých XML dokumentech může být pořadí elementů důležité, pro Atom feed to neplatí.
Po metadatech vázaných na celý dokument (feed) se nachází seznam nejnovějších článků. Článek vypadá takto:
<entry>
<author> ①
<name>Mark</name>
<uri>http://diveintomark.org/</uri>
</author>
<title>Dive into history, 2009 edition</title> ②
<link rel='alternate' type='text/html' ③
href='http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition'/>
<id>tag:diveintomark.org,2009-03-27:/archives/20090327172042</id> ④
<updated>2009-03-27T21:56:07Z</updated> ⑤
<published>2009-03-27T17:20:42Z</published>
<category scheme='http://diveintomark.org' term='diveintopython'/> ⑥
<category scheme='http://diveintomark.org' term='docbook'/>
<category scheme='http://diveintomark.org' term='html'/>
<summary type='html'>Putting an entire chapter on one page sounds ⑦
bloated, but consider this &mdash; my longest chapter so far
would be 75 printed pages, and it loads in under 5 seconds&hellip;
On dialup.</summary>
</entry> ⑧
author
říká, kdo článek napsal: nějaký maník jménem Mark, který se poflakuje někde na http://diveintomark.org/
. (Je to stejná hodnota, jako alternativní odkaz v metadatech k feed, ale nemusí tomu tak být. Mnoho weblogů využívá více autorů najednou a každý z nich mívá jiný osobní webový server.)
title
nese název článku — „Dive into history, 2009 edition“.
link
obsahuje adresu HTML verze tohoto článku, podobně jako v případě alternativního odkazu na úrovni celého obsahu (feed).
entry
), stejně jako celý obsah (feed), potřebují jednoznačný identifikátor.
published
) a datum poslední modifikace (updated
).
category
). Tento článek je zařazen pod diveintopython
, docbook
a html
.
summary
nese stručné shrnutí obsahu článku. (Existuje i element content
— tj. obsah —, který zde není použit. Je určen pro vložení celého textu článku.) Tento element summary
nese atribut type='html'
, který je specifický pro Atom. Říká, že uvedené shrnutí není prostý text, ale úryvek ve formátu HTML. Ta informace je důležitá, protože se v něm nacházejí věci specifické pro HTML (—
a …
), které se nemají zviditelňovat jako text, ale jako „—“ a „…“.
entry
, která signalizuje konec metadat pro tento článek.
⁂
Python dovede analyzovat XML dokumenty několika způsoby. Najdeme zde tradiční syntaktické analyzátory (také parsery) DOM a SAX. My se ale zaměříme na jinou knihovnu zvanou ElementTree.
>>> import xml.etree.ElementTree as etree ① >>> tree = etree.parse('examples/feed.xml') ② >>> root = tree.getroot() ③ >>> root ④ <Element {http://www.w3.org/2005/Atom}feed at cd1eb0>
xml.etree.ElementTree
.
parse()
, která přebírá buď jméno souboru nebo souboru se podobající objekt. Funkce zpracuje celý dokument najednou. Pokud chceme šetřit pamětí, existují způsoby, jak můžeme XML dokument zpracovávat postupně.
parse()
vrací objekt, který reprezentuje celý dokument. Ale není to kořenový element. Pokud chceme získat odkaz na kořenový element, zavoláme metodu getroot()
.
feed
, který se nachází v prostoru jmen http://www.w3.org/2005/Atom
. Řetězcová reprezentace tohoto objektu v nás posiluje důležitý pohled: XML element je kombinací svého prostoru jmen a jména své značky (která se též nazývá lokální jméno). Každý element tohoto dokumentu se nachází v prostoru jmen Atom, takže kořenový element je reprezentován jako {http://www.w3.org/2005/Atom}feed
.
☞ElementTree reprezentuje XML elementy jako
{prostor_jmen}lokální_jméno
. Tento formát uvidíme a budeme používat na mnoha místech aplikačního rozhraní ElementTree.
V aplikačním rozhraní ElementTree se elementy chovají jako seznamy. Položkami seznamu jsou elementy potomků (child).
# pokračování předchozího příkladu >>> root.tag ① '{http://www.w3.org/2005/Atom}feed' >>> len(root) ② 8 >>> for child in root: ③ ... print(child) ④ ... <Element {http://www.w3.org/2005/Atom}title at e2b5d0> <Element {http://www.w3.org/2005/Atom}subtitle at e2b4e0> <Element {http://www.w3.org/2005/Atom}id at e2b6c0> <Element {http://www.w3.org/2005/Atom}updated at e2b6f0> <Element {http://www.w3.org/2005/Atom}link at e2b4b0> <Element {http://www.w3.org/2005/Atom}entry at e2b720> <Element {http://www.w3.org/2005/Atom}entry at e2b510> <Element {http://www.w3.org/2005/Atom}entry at e2b750>
{http://www.w3.org/2005/Atom}feed
.
title
, subtitle
, id
, updated
a link
) následovaná třemi elementy entry
.
Asi už jste to odhadli, ale zdůrazněme to ještě explicitně: seznam dětských elementů zahrnuje pouze přímé potomky. Každý z elementů entry
obsahuje své vlastní potomky, ale ti v tomto seznamu uvedeni nejsou. Jako dětské elementy jsou součástí seznamů elementů entry
, ale nejsou zahrnuty mezi potomky elementu feed
. Existují způsoby, jak můžeme elementy vyhledat nezávisle na tom, jak hluboko jsou zanořené. Na dva takové způsoby se v této kapitole podíváme později.
XML není jen kolekcí elementů. Každý element má svou vlastní sadu atributů. Jakmile máme odkaz na konkrétní element, můžeme jeho atributy snadno získat jako pythonovský slovník.
# pokračování předchozího příkladu >>> root.attrib ① {'{http://www.w3.org/XML/1998/namespace}lang': 'en'} >>> root[4] ② <Element {http://www.w3.org/2005/Atom}link at e181b0> >>> root[4].attrib ③ {'href': 'http://diveintomark.org/', 'type': 'text/html', 'rel': 'alternate'} >>> root[3] ④ <Element {http://www.w3.org/2005/Atom}updated at e2b4e0> >>> root[3].attrib ⑤ {}
attrib
je slovníkem atributů elementu. Původní značka vypadala takto: <feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
. Prefix xml:
se vztahuje k zabudovanému prostoru jmen, který můžeme používat v každém XML dokumentu, aniž bychom jej museli deklarovat.
[4]
odpovídá indexování seznamu od nuly — je element link
.
link
má tři atributy: href
, type
a rel
.
[3]
odpovídá indexování seznamu od nuly — je element updated
.
updated
nemá žádné atributy, takže jeho vlastnost .attrib
je prostě prázdný slovník.
⁂
Zatím jsme s uvedeným XML dokumentem pracovali „shora dolů“. Začali jsme u kořenového elementu, zpřístupnili jsme si elementy jeho potomků a tak dále napříč dokumentem. Ale při mnoha použitích XML se požaduje nalezení určitého elementu. Etree to umí také.
>>> import xml.etree.ElementTree as etree >>> tree = etree.parse('examples/feed.xml') >>> root = tree.getroot() >>> root.findall('{http://www.w3.org/2005/Atom}entry') ① [<Element {http://www.w3.org/2005/Atom}entry at e2b4e0>, <Element {http://www.w3.org/2005/Atom}entry at e2b510>, <Element {http://www.w3.org/2005/Atom}entry at e2b540>] >>> root.tag '{http://www.w3.org/2005/Atom}feed' >>> root.findall('{http://www.w3.org/2005/Atom}feed') ② [] >>> root.findall('{http://www.w3.org/2005/Atom}author') ③ []
findall()
najde všechny dětské elementy, které odpovídají určitému dotazu. (O formátu dotazu si řekneme za minutku.)
findall()
. Ta mezi potomky najde všechny odpovídající elementy. Ale proč tu nejsou žádné výsledky? Ačkoliv to nemusí být úplně zřejmé, tento dotaz prohledává jen elementy potomků. A protože kořenový element feed
nemá žádného potomka jménem feed
, vrací dotaz prázdný seznam.
author
. Ve skutečnosti jsou v něm tři (jeden v každém elementu entry
). Ale elementy author
nejsou přímými potomky kořenového elementu. Jsou to jeho „vnuci“ (doslova potomci potomků). Pokud hledáte elementy author
na libovolné úrovni zanoření, je to možné provést, ale formát dotazu se mírně liší.
>>> tree.findall('{http://www.w3.org/2005/Atom}entry') ① [<Element {http://www.w3.org/2005/Atom}entry at e2b4e0>, <Element {http://www.w3.org/2005/Atom}entry at e2b510>, <Element {http://www.w3.org/2005/Atom}entry at e2b540>] >>> tree.findall('{http://www.w3.org/2005/Atom}author') ② []
tree
(vracený funkcí etree.parse()
) několik metod, které odpovídají metodám kořenového elementu. Výsledky jsou stejné, jako kdybychom zavolali metodu tree.getroot().findall()
.
author
. Proč ne? Protože je to zkratka pro tree.getroot().findall('{http://www.w3.org/2005/Atom}author')
, což znamená „najdi všechny elementy author
, které jsou potomky kořenového elementu“. Elementy author
nejsou potomky kořenového elementu. Jsou to potomci elementů entry
. Takže uvedený dotaz nenajde žádnou shodu.
Existuje také metoda find()
, která vrací první vyhovující element. Hodí se v situacích, kdy očekáváme pouze jeden výskyt, nebo když je výskytů víc, ale zajímá nás jen první.
>>> entries = tree.findall('{http://www.w3.org/2005/Atom}entry') ① >>> len(entries) 3 >>> title_element = entries[0].find('{http://www.w3.org/2005/Atom}title') ② >>> title_element.text 'Dive into history, 2009 edition' >>> foo_element = entries[0].find('{http://www.w3.org/2005/Atom}foo') ③ >>> foo_element >>> type(foo_element) <class 'NoneType'>
atom:entry
.
find()
přebírá dotaz a vrací první vyhovující element.
foo
, takže se vrací None
.
☞S metodou
find()
je spojen „chyták“, který vás jednou dostane. Objekt elementu z ElementTree se v booleovském kontextu vyhodnocuje jakoFalse
v případě, kdy neobsahuje žádné potomky (tj. jestliželen(element)
je rovno nule). To znamená, že zápisif element.find('...')
netestuje, zda metodafind()
nalezla vyhovující element. Testuje, zda vyhovující element má nějaké potomky! Pokud chceme testovat, zda metodafind()
vrátila nějaký element, musíme použít zápisif element.find('...') is not None
.
On ale existuje způsob, jak najít elementy veškerých příbuzných potomků, tj. dětí, vnuků a dalších elementů na libovolné úrovni zanoření.
>>> all_links = tree.findall('//{http://www.w3.org/2005/Atom}link') ① >>> all_links [<Element {http://www.w3.org/2005/Atom}link at e181b0>, <Element {http://www.w3.org/2005/Atom}link at e2b570>, <Element {http://www.w3.org/2005/Atom}link at e2b480>, <Element {http://www.w3.org/2005/Atom}link at e2b5a0>] >>> all_links[0].attrib ② {'href': 'http://diveintomark.org/', 'type': 'text/html', 'rel': 'alternate'} >>> all_links[1].attrib ③ {'href': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition', 'type': 'text/html', 'rel': 'alternate'} >>> all_links[2].attrib {'href': 'http://diveintomark.org/archives/2009/03/21/accessibility-is-a-harsh-mistress', 'type': 'text/html', 'rel': 'alternate'} >>> all_links[3].attrib {'href': 'http://diveintomark.org/archives/2008/12/18/give-part-1-container-formats', 'type': 'text/html', 'rel': 'alternate'}
//{http://www.w3.org/2005/Atom}link
— je těm z předchozích příkladů velmi podobný. Jedinou odlišností jsou dvě lomítka na začátku dotazu. Tato dvě lomítka znamenají: „Nedívej se jen na přímé potomky. Chci najít jakékoliv elementy, nezávisle na úrovni zanoření.“ Takže výsledkem je seznam čtyř elementů link
a nejen jednoho.
entry
. Každý element entry
obsahuje jeden dětský element link
. A protože je na začátku dotazu uvedena dvojice lomítek, najde dotaz všechny.
Celkově vzato je metoda findall()
objektu třídy ElementTree velmi mocným nástrojem, ale dotazovací jazyk může přinést pár překvapení. Oficiálně se o něm píše jako o „omezené podpoře výrazů XPath”. XPath je W3C standardem pro dotazování v XML dokumentech. Dotazovací jazyk implementovaný třídou ElementTree se XPath podobá do té míry, že se hodí pro základní vyhledávání. Ale pokud už znáte XPath, mohou vás rozdíly rozčilovat. Teď se podíváme na XML knihovnu třetí strany, která rozšiřuje aplikační rozhraní ElementTree o plnou podporu XPath.
⁂
lxml
je open source knihovna třetí strany, která je vybudována nad populárním parserem libxml2. Poskytuje aplikační rozhraní, které je 100% slučitelné s ElementTree a rozšiřuje ho o plnou podporu XPath 1.0 a o pár dalších vylepšení. K dispozici jsou instalátory pro Windows. Uživatelé Linuxu by měli zkusit nainstalovat předkompilovaný binární tvar z archivů prostřednictvím nástrojů příslušné distribuce, jako je třeba yum
nebo apt-get
. Pokud by to nešlo, museli byste lxml
nainstalovat ručně.
>>> from lxml import etree ① >>> tree = etree.parse('examples/feed.xml') ② >>> root = tree.getroot() ③ >>> root.findall('{http://www.w3.org/2005/Atom}entry') ④ [<Element {http://www.w3.org/2005/Atom}entry at e2b4e0>, <Element {http://www.w3.org/2005/Atom}entry at e2b510>, <Element {http://www.w3.org/2005/Atom}entry at e2b540>]
lxml
naimportujeme, máme k dispozici stejné aplikační rozhraní jako u zabudované knihovny ElementTree.
parse()
— stejná jako u ElementTree.
getroot()
— také stejná.
findall()
— naprosto stejná.
Pro velké XML dokumenty je lxml
výrazně rychlejší než zabudovaná knihovna ElementTree. Pokud používáte pouze aplikační rozhraní ElementTree a chcete používat nejrychlejší dostupnou implementaci, můžete vyzkoušet naimportovat lxml
se záchranou v podobě zabudované ElementTree.
try:
from lxml import etree
except ImportError:
import xml.etree.ElementTree as etree
Ale lxml
je víc než pouhá rychlejší podoba ElementTree. Její implementace metody findall()
podporuje komplikovanější výrazy.
>>> import lxml.etree ① >>> tree = lxml.etree.parse('examples/feed.xml') >>> tree.findall('//{http://www.w3.org/2005/Atom}*[@href]') ② [<Element {http://www.w3.org/2005/Atom}link at eeb8a0>, <Element {http://www.w3.org/2005/Atom}link at eeb990>, <Element {http://www.w3.org/2005/Atom}link at eeb960>, <Element {http://www.w3.org/2005/Atom}link at eeb9c0>] >>> tree.findall("//{http://www.w3.org/2005/Atom}*[@href='http://diveintomark.org/']") ③ [<Element {http://www.w3.org/2005/Atom}link at eeb930>] >>> NS = '{http://www.w3.org/2005/Atom}' >>> tree.findall('//{NS}author[{NS}uri]'.format(NS=NS)) ④ [<Element {http://www.w3.org/2005/Atom}author at eeba80>, <Element {http://www.w3.org/2005/Atom}author at eebba0>]
import lxml.etree
. Chceme zde zdůraznit, že jde o vlastnosti specifické pro lxml
(takže nenapíšeme, dejme tomu, from lxml import etree
).
href
— ať už se nacházejí v dokumentu kdekoliv. Dvě lomítka (//
) na začátku dotazu znamenají „elementy nacházející se kdekoliv (ne jenom potomci nebo kořenový element)“. {http://www.w3.org/2005/Atom}
znamená „jen elementy z prostoru jmen Atom“. *
znamená „elementy s libovolným lokálním jménem“. A [@href]
znamená, „které mají atribut href
”.
href
s hodnotou http://diveintomark.org/
.
author
, které mají mezi svými potomky element uri
. Vrátí se jen dva elementy author
— jen z prvního a druhého elementu entry
. Element author
v posledním entry
obsahuje jen name
— uri
mu chybí.
Ještě toho nemáte dost? Do lxml
je zahrnuta i podpora pro libovolné výrazy XPath 1.0. Nebudu se do hloubky zabývat syntaxí XPath. To by samo o sobě vydalo na celou knihu! Ale ukážeme si, jakým způsobem je podpora XPath do lxml
zahrnuta.
>>> import lxml.etree >>> tree = lxml.etree.parse('examples/feed.xml') >>> NSMAP = {'atom': 'http://www.w3.org/2005/Atom'} ① >>> entries = tree.xpath("//atom:category[@term='accessibility']/..", ② ... namespaces=NSMAP) >>> entries ③ [<Element {http://www.w3.org/2005/Atom}entry at e2b630>] >>> entry = entries[0] >>> entry.xpath('./atom:title/text()', namespaces=NSMAP) ④ ['Accessibility is a harsh mistress']
category
(z prostoru jmen Atom), které obsahují atribut term
s hodnotou accessibility
. To ale ještě není výsledkem dotazu. Podívejte se na úplný konec řetězce dotazu. Všimli jste si úseku /..
? Ten znamená „a vrať k právě nalezenému elementu category
jeho rodičovský element“. Takže tento jediný dotaz XPath najde všechny elementy potomky <category term='accessibility'>
.
xpath()
vrací seznam objektů třídy ElementTree. V tomto dokumentu se nachází jediný záznam obsahující category
, jehož term
má hodnotu accessibility
.
text()
) elementu title
(atom:title
), který je potomkem aktuálního elementu (./
).
⁂
Podpora XML v Pythonu není omezena na analýzu (parsing) existujících dokumentů. Můžeme také vytvářet XML dokumenty zcela od základů.
>>> import xml.etree.ElementTree as etree >>> new_feed = etree.Element('{http://www.w3.org/2005/Atom}feed', ① ... attrib={'{http://www.w3.org/XML/1998/namespace}lang': 'en'}) ② >>> print(etree.tostring(new_feed)) ③ <ns0:feed xmlns:ns0='http://www.w3.org/2005/Atom' xml:lang='en'/>
Element
. Jako první argument předáváme jméno elementu (prostor jmen + lokální jméno). Tímto příkazem se vytvoří element feed
v prostoru jmen Atom. To bude kořenový element našeho nového dokumentu.
{prostor jmen}lokální jméno
.
tostring()
z ElementTree.
Jste výsledkem serializace překvapeni? Způsob, jakým ElementTree serializuje XML elementy s prostorem jmen, je sice z technického hlediska přesný, ale není optimální. Vzorový XML dokument ze začátku této kapitoly definoval výchozí prostor jmen (xmlns='http://www.w3.org/2005/Atom'
). U dokumentů, kde se všechny elementy nacházejí ve stejném prostoru jmen — jako u Atom feeds — je definice výchozího prostoru jmen užitečná, protože ji uvedeme jen jednou a elementy pak můžeme deklarovat jen jejich lokálním jménem (<feed>
, <link>
, <entry>
). Pokud nepotřebujeme deklarovat elementy z jiného prostoru jmen, nemusíme prefixy uvádět.
XML parser „nevidí“ mezi XML dokumentem s výchozím prostorem jmen a mezi XML dokumentem s prefixovaným prostorem jmen žádný rozdíl. Výsledný DOM s následující serializací:
<ns0:feed xmlns:ns0='http://www.w3.org/2005/Atom' xml:lang='en'/>
je totožný s DOM s touto serializací:
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'/>
Jediný praktický rozdíl spočívá v tom, že druhá serializace je o pár znaků kratší. Kdybychom chtěli celý vzorek našeho obsahu (feed) přepsat s prefixem ns0:
v každé počáteční a koncové značce, přidalo by to 4 znaky na každou značku × 79 značek + 4 znaky pro vlastní deklaraci prostoru jmen, to je celkem 320 znaků. Za předpokladu, že používáme kódování UTF-8, to je 320 bajtů navíc. (Po zabalení pomocí gzip se rozdíl zmenší na 21 bajtů, ale 21 bajtů je pořád 21 bajtů.) Pro vás to možná nic neznamená, ale pro něco takového jako je Atom feed, který může být stahován několikatisíckrát, kdykoliv dojde ke změně, se může úspora pár bajtů na dotaz rychle nasčítat.
Zabudovaná knihovna ElementTree tak jemné ovládání serializace elementů z prostoru jmen nenabízí, ale lxml
ano.
>>> import lxml.etree >>> NSMAP = {None: 'http://www.w3.org/2005/Atom'} ① >>> new_feed = lxml.etree.Element('feed', nsmap=NSMAP) ② >>> print(lxml.etree.tounicode(new_feed)) ③ <feed xmlns='http://www.w3.org/2005/Atom'/> >>> new_feed.set('{http://www.w3.org/XML/1998/namespace}lang', 'en') ④ >>> print(lxml.etree.tounicode(new_feed)) <feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'/>
None
v roli klíče definujeme výchozí prostor jmen.
lxml
), bude lxml
respektovat prefixy prostorů jmen, které jsme definovali.
feed
bez prefixu prostoru jmen.
xml:lang
. Libovolný atribut můžeme k libovolnému elementu přidat metodou set()
. Přebírá dva argumenty: jméno atributu ve formátu pro ElementTree a hodnotu atributu. (Tato metoda není specifická pro lxml
. Jedinou částí specifickou pro lxml
byl v tomto příkladu argument nsmap, který v serializovaném výstupu ovládá prefixování prostorem jmen.)
Může se v XML dokumentech vyskytovat jen jeden element na dokument? Samozřejmě že ne. Snadno můžeme vytvořit i elementy potomků.
>>> title = lxml.etree.SubElement(new_feed, 'title', ① ... attrib={'type':'html'}) ② >>> print(lxml.etree.tounicode(new_feed)) ③ <feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'><title type='html'/></feed> >>> title.text = 'dive into …' ④ >>> print(lxml.etree.tounicode(new_feed)) ⑤ <feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'><title type='html'>dive into &hellip;</title></feed> >>> print(lxml.etree.tounicode(new_feed, pretty_print=True)) ⑥ <feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'> <title type='html'>dive into&hellip;</title> </feed>
SubElement
. Jedinými povinnými argumenty jsou zde rodičovský element (v našem případě new_feed) a jméno nového elementu. Protože má dětský element dědit mapování (zobrazení) prostoru jmen od svého rodiče, nemusíme zde prostoj jmen nebo prefix znovu deklarovat.
title
a byl vložen jako potomek do elementu feed
. Protože element title
neobsahoval žádný text a neměl své vlastní potomky, serializuje jej lxml
jako prázdný element (zkrácený zápis s />
na konci).
.text
.
title
serializuje i se svým textovým obsahem. Každý text, který obsahuje znaky menší než nebo ampersand, musí být při serializaci převeden na speciální posloupnosti. lxml
se o to postará automaticky.
lxml
přidá „nevýznamné bílé znaky“ za účelem zvýšení čitelnosti výstupu.
☞Možná byste se chtěli mrknout také na xmlwitch, což je další knihovna třetí strany pro generování XML. Aby byl kód pro generování XML čitelnější, široce se v ní využívá příkazu
with
.
⁂
Specifikace XML nařizuje, aby všechny XML parsery, které chtějí specifikaci vyhovět, používaly „drakonickou obsluhu chyb“. To znamená, že musí s výrazným efektem zastavit, jakmile v XML dokumentu narazí na jakýkoliv prohřešek proti korektní podobě. Prohřešky proti správné formě zahrnují nespárované počáteční a koncové značky, nedefinované entity (speciální posloupnosti pro znaky), nelegální Unicode znaky a řadu dalších esoterických pravidel. To je v příkrém kontrastu s jinými běžnými formáty, jako je například HTML. Váš prohlížeč nepřestane zobrazovat stránku, ve které zapomenete uvést uzavírací značku HTML nebo když zapomenete zapsat ampersand v atributu jako speciální sekvenci. (Běžným omylem je, že HTML nemá definováno ošetření chyb. Ošetřování chyb v HTML je ve skutečnosti definováno velmi dobře, ale je výrazně komplikovanější, než „zastav a začni hořet“ v okamžiku, kdy se narazí na první chybu.)
Někteří lidé věří (a já patřím mezi ně), že požadavek na drakonickou obsluhu chyb byl ze strany tvůrců XML nepřiměřený. Nechápejte mě špatně. Zjednodušení pravidel pro ošetření chyb má své kouzlo. Ale v praxi je koncepce „korektnosti formátu“ ošidnější, než to vypadá — zvlášť u XML (jako je Atom feeds), které jsou zveřejňovány na webu a zpřístupňovány protokolem HTTP. I přes vyzrálost formátu XML, který standardizoval drakonická pravidla pro ošetřování chyb v roce 1997, průzkumy stále ukazují, že významná část dokumentů Atom feeds nacházejících se na webu je zamořena chybami formátu.
Takže mám jak teoretické, tak praktické důvody ke zpracování (parse) XML dokumentů „za každou cenu“. To znamená, že nechci s kraválem zastavit při prvním prohřešku proti korektnosti formátu. Pokud zjistíte, že to cítíte podobně, může vám pomoci lxml
.
Tady máme kousek porušeného XML dokumentu. Prohřešky proti korektnosti jsem zvýraznil.
<?xml version='1.0' encoding='utf-8'?>
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
<title>dive into …</title>
...
</feed>
Tak tohle je chyba, protože entita …
není v XML definována. (Je definována v HTML.) Pokud se takto porušený obsah (feed) pokusíte zpracovat (parse), lxml
se zakucká na nedefinované entitě.
>>> import lxml.etree >>> tree = lxml.etree.parse('examples/feed-broken.xml') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "lxml.etree.pyx", line 2693, in lxml.etree.parse (src/lxml/lxml.etree.c:52591) File "parser.pxi", line 1478, in lxml.etree._parseDocument (src/lxml/lxml.etree.c:75665) File "parser.pxi", line 1507, in lxml.etree._parseDocumentFromURL (src/lxml/lxml.etree.c:75993) File "parser.pxi", line 1407, in lxml.etree._parseDocFromFile (src/lxml/lxml.etree.c:75002) File "parser.pxi", line 965, in lxml.etree._BaseParser._parseDocFromFile (src/lxml/lxml.etree.c:72023) File "parser.pxi", line 539, in lxml.etree._ParserContext._handleParseResultDoc (src/lxml/lxml.etree.c:67830) File "parser.pxi", line 625, in lxml.etree._handleParseResult (src/lxml/lxml.etree.c:68877) File "parser.pxi", line 565, in lxml.etree._raiseParseError (src/lxml/lxml.etree.c:68125) lxml.etree.XMLSyntaxError: Entity 'hellip' not defined, line 3, column 28
Abychom byli schopni takto porušený XML dokument zpracovat (navzdory prohřešku proti korektnímu formátu), musíme vytvořit vlastní XML parser.
>>> parser = lxml.etree.XMLParser(recover=True) ① >>> tree = lxml.etree.parse('examples/feed-broken.xml', parser) ② >>> parser.error_log ③ examples/feed-broken.xml:3:28:FATAL:PARSER:ERR_UNDECLARED_ENTITY: Entity 'hellip' not defined >>> tree.findall('{http://www.w3.org/2005/Atom}title') [<Element {http://www.w3.org/2005/Atom}title at ead510>] >>> title = tree.findall('{http://www.w3.org/2005/Atom}title')[0] >>> title.text ④ 'dive into ' >>> print(lxml.etree.tounicode(tree.getroot())) ⑤ <feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'> <title>dive into </title> . . [rest of serialization snipped for brevity] .
lxml.etree.XMLParser
. Lze jí předat celou řadu pojmenovaných argumentů. Nás momentálně zajímá argument recover. Pokud jej nastavíme na hodnotu True
, XML parser udělá, co je v jeho silách, aby se z chyb proti korektnímu formátu „zotavil“.
parse()
jako druhý argument. Všimněte si, že lxml
kvůli nedefinované entitě …
nevyvolal žádnou výjimku.
…
dělat, parser ji jednoduše vypustil. Takže textový obsah, který se nachází za elementem title
, se změní na 'dive into '
.
…
se nikam nepřesunula. Byla jednoduše vypuštěna.
Pokud používáme syntaktické analyzátory XML se „zotavením“, pak je nutné znovu zopakovat, že neexistuje žádná záruka vzájemné součinnosti. Jiný parser se mohl rozhodnout, že jde o entitu …
z HTML, a nahradí ji posloupností &hellip;
. Je to „lepší“? Možná. Je to „správnější“? Ne. Oba případy jsou stejně nesprávné. Správné chování (podle specifikace XML) spočívá v tom, že parser „zastaví a začne hořet“. Pokud jste se rozhodli, že to neuděláte, je to vaše věc.
⁂
lxml
lxml
lxml
© 2001–11 Mark Pilgrim