Nacházíte se zde: Domů Ponořme se do Pythonu 3

Úroveň obtížnosti: ♦♦♦♦♢

XML

In the archonship of Aristaechmus, Draco enacted his ordinances.
(Za vlády Aristaechma uzákonil Drakon svá pravidla.)
Aristoteles

 

Ponořme se

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.

[stáhnout feed.xml]

<?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 &amp;mdash; my longest chapter so far
    would be 75 printed pages, and it loads in under 5 seconds&amp;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>

Pětiminutový rychlokurz XML

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>  
  1. Toto je počáteční značka elementu foo.
  2. Toto je odpovídající koncová značka elementu 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>
  1. Element foo má jeden atribut pojmenovaný lang. Hodnotou jeho atributu lang je en.
  2. Element 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>
  1. Element feed se nachází v prostoru jmen http://www.w3.org/2005/Atom.
  2. Element 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>
  1. Element feed se nachází v prostoru jmen http://www.w3.org/2005/Atom.
  2. Element 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í!

Struktura Atom Feed

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'>                       
  1. http://www.w3.org/2005/Atom je prostor jmen pro Atom.
  2. Libovolný element může obsahovat atribut 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/'/>  
  1. Nadpis obsahu je dive into mark.
  2. Podnadpis obsahu je currently between addictions.
  3. Každý obsah (feed) potřebuje globálně jednoznačný identifikátor. V dokumentu RFC 4151 najdete, jak se dá vytvořit.
  4. Tento obsah byl naposledy aktualizován 27. března 2009 v 21.56 GMT. Obvykle se shoduje s časem poslední modifikace nejnovějšího článku.
  5. Teď to začne být zajímavé. Tento element 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 &amp;mdash; my longest chapter so far
    would be 75 printed pages, and it loads in under 5 seconds&amp;hellip;
    On dialup.</summary>
</entry>                                                                   
  1. Element 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.)
  2. Element title nese název článku — „Dive into history, 2009 edition“.
  3. Element link obsahuje adresu HTML verze tohoto článku, podobně jako v případě alternativního odkazu na úrovni celého obsahu (feed).
  4. Položky (entry), stejně jako celý obsah (feed), potřebují jednoznačný identifikátor.
  5. Položky nesou dvě data: datum prvního zveřejnění (published) a datum poslední modifikace (updated).
  6. Položky mohou nést libovolný počet kategorií (category). Tento článek je zařazen pod diveintopython, docbook a html.
  7. Element 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 (&mdash; a &hellip;), které se nemají zviditelňovat jako text, ale jako „—“ a „…“.
  8. A na závěr je tu koncová značka elementu entry, která signalizuje konec metadat pro tento článek.

Analýza XML

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.

[download feed.xml]

>>> 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>
  1. Knihovna ElementTree je součástí standardní pythonovské knihovny. Nachází se v xml.etree.ElementTree.
  2. Primárním vstupním bodem knihovny ElementTree je funkce 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ě.
  3. Funkce 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().
  4. Jak se dalo čekat, kořenovým elementem je element 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.

Elementy jsou reprezentovány seznamy

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>
  1. Pokračujme v předchozím příkladu. Kořenový element je {http://www.w3.org/2005/Atom}feed.
  2. „Délkou“ kořenového elementu rozumíme počet dětských elementů (potomků, child).
  3. Objekt elementu můžeme použít jako iterátor, který zajistí průchod všemi svými dětskými elementy.
  4. Na výstupu vidíme, že obsahuje očekávaných 8 potomků: metadata patřící k 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.

Atributy jsou reprezentovány slovníky

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                        
{}
  1. Vlastnost 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.
  2. Pátým potomkem — [4] odpovídá indexování seznamu od nuly —  je element link.
  3. Element link má tři atributy: href, type a rel.
  4. Čtvrtým potomkem — [3] odpovídá indexování seznamu od nuly —  je element updated.
  5. Element updated nemá žádné atributy, takže jeho vlastnost .attrib je prostě prázdný slovník.

Vyhledávání uzlů v XML dokumentu

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')   
[]
  1. Metoda findall() najde všechny dětské elementy, které odpovídají určitému dotazu. (O formátu dotazu si řekneme za minutku.)
  2. Každý element — včetně kořenového elementu, ale také dětských elementů — má metodu 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.
  3. Tento výsledek vás možná také překvapí. V tomto dokumentu se nachází element 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')   
[]
  1. Z praktických důvodů má objekt 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().
  2. Tento dotaz, možná trošku překvapivě, v dokumentu nenajde elementy 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'>
  1. Tohle jsme viděli v předchozím příkladu. Naleznou se všechny elementy atom:entry.
  2. Metoda find() přebírá dotaz a vrací první vyhovující element.
  3. Uvnitř elementu nejsou žádné položky nazvané 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 jako False v případě, kdy neobsahuje žádné potomky (tj. jestliže len(element) je rovno nule). To znamená, že zápis if element.find('...') netestuje, zda metoda find() nalezla vyhovující element. Testuje, zda vyhovující element má nějaké potomky! Pokud chceme testovat, zda metoda find() vrátila nějaký element, musíme použít zápis if 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'}
  1. Tento dotaz — //{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.
  2. První výsledek je přímým potomkem kořenového elementu. Jak vidíme z jeho atributů, jde o alternativní odkaz z úrovně celého obsahu (feed). Odkazuje na HTML verzi webového místa, které zveřejňovaný obsah popisuje.
  3. Ostatní tři výsledky jsou alternativní odkazy z každého elementu 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 jde ještě dál

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>]
  1. Jakmile lxml naimportujeme, máme k dispozici stejné aplikační rozhraní jako u zabudované knihovny ElementTree.
  2. Funkce parse() — stejná jako u ElementTree.
  3. Metoda getroot() — také stejná.
  4. Metoda 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>]
  1. V tomto příkladu provedeme import lxml.etree. Chceme zde zdůraznit, že jde o vlastnosti specifické pro lxml (takže nenapíšeme, dejme tomu, from lxml import etree).
  2. Tento dotaz najde všechny elementy z prostoru jmen Atom, které mají atribut 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”.
  3. Tento dotaz najde všechny elementy z Atom, které mají href s hodnotou http://diveintomark.org/.
  4. S využitím jednoduchého formátovacího řetězce (protože jinak by se tyto složené dotazy staly neúnosně dlouhé) získáme dotaz, který v prostoru Atom hledá elementy 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 nameuri 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']
  1. Abychom mohli provádět dotazy XPath nad elementy z nějakého prostoru jmen, musíme definovat zobrazení prefixu na prostor jmen. Je to prostě pythonovský slovník.
  2. Tady máme dotaz v XPath. Výraz v XPath hledá elementy 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'>.
  3. Funkce xpath() vrací seznam objektů třídy ElementTree. V tomto dokumentu se nachází jediný záznam obsahující category, jehož term má hodnotu accessibility.
  4. XPath výraz nevrací vždycky seznam elementů. DOM (Document Object Model; objektový model dokumentu), který vznikl na základě zpracování (parsing) XML dokumentu, neobsahuje z technického hlediska elementy, ale uzly. Uzly mohou (podle typu) reprezentovat elementy, atributy nebo dokonce textový obsah. Výsledkem XPath dotazu je seznam uzlů. Tento dotaz vrací seznam textových uzlů: textový obsah (text()) elementu title (atom:title), který je potomkem aktuálního elementu (./).

Generování XML

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'/>
  1. Nový element vznikne vytvořením instance třídy 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.
  2. Atributy k nově vytvořenému elementu přidáme předáním slovníku se jmény a hodnotami atributů argumentem attrib. Poznamenejme, že jména atributů musí být uvedena ve tvaru pro ElementTree — {prostor jmen}lokální jméno.
  3. Kterýkoliv element (a jeho potomky) můžeme kdykoliv převést na řetězec (serializovat) voláním funkce 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'/>
  1. Začneme tím, že definujeme zobrazení prostorů jmen v podobě slovníku. Hodnotami slovníku jsou prostory jmen, klíči jsou požadované prefixy. Použitím None v roli klíče definujeme výchozí prostor jmen.
  2. Když teď při vytváření elementu předáme slovník argumentem nsmap (je specifický pro lxml), bude lxml respektovat prefixy prostorů jmen, které jsme definovali.
  3. Tato serializace podle očekávání definuje prostor jmen Atom jako výchozí prostor jmen a deklaruje element feed bez prefixu prostoru jmen.
  4. Jejda! Zapomněli jsme přidat atribut 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 &hellip;'                         
>>> print(lxml.etree.tounicode(new_feed))                     
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'><title type='html'>dive into &amp;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&amp;hellip;</title>
</feed>
  1. Při vytváření dětského elementu k existujícímu elementu vytváříme instanci třídy 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.
  2. Můžeme také předat slovník atributů. Klíče hrají roli jmen atributů, hodnoty jsou hodnotami atributů.
  3. Podle očekávání byl v prostoru jmen Atom vytvořen element 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).
  4. Pokud chceme elementu nastavit textový obsah, přiřadíme jej jednoduše do vlastnosti .text.
  5. Teď už se element 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.
  6. Při serializaci můžeme předepsat také „tisk v pěkném tvaru“. Za koncové značky a za počáteční značky elementů, které obsahují potomky, ale ne text, se vloží přechody na nový řádek. Vyjádřeno technickými pojmy, 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.

Analýza porušeného XML

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 &hellip;</title>
...
</feed>

Tak tohle je chyba, protože entita &hellip; 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]
.
  1. Uživatelský parser (syntaktický analyzátor) vznikne vytvořením instance třídy 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“.
  2. Náš XML dokument zpracujeme pomocí uživatelského parseru tak, že objekt parser předáme funkci parse() jako druhý argument. Všimněte si, že lxml kvůli nedefinované entitě &hellip; nevyvolal žádnou výjimku.
  3. Syntaktický analyzátor veškeré prohřešky proti korektnímu formátu zaznamenává. (Ve skutečnosti je zaznamenává nezávisle na tom, zda jsme mu nastavili zotavovací režim po chybě nebo ne.)
  4. Protože nevěděl, co má s nedefinovanou entitou &hellip; dělat, parser ji jednoduše vypustil. Takže textový obsah, který se nachází za elementem title, se změní na 'dive into '.
  5. Jak vidíte ze serializované hodnoty, entita &hellip; 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 &hellip; z HTML, a nahradí ji posloupností &amp;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.

Přečtěte si

© 2001–11 Mark Pilgrim