Hlavní navigace

Zpracování XML v Pythonu s využitím knihovny lxml

Pavel Tišnovský

Vývojáři se poměrně často setkají s požadavkem na zpracování souborů XML nebo HTML. V Pythonu přitom mají na výběr z několika knihoven, z nichž nejsnáze použitelná je knihovna nazvaná lxml.

Doba čtení: 27 minut

11. Použití relativních cest

12. Přístup k hodnotám uzlů s využitím XPath

13. Získání cesty k vybranému uzlu pro pozdější použití

14. Základy zpracování HTML stránek

15. Použití objektu typu HTMLParser

16. Přečtení obsahu všech odstavců, získání atributů prvků atd.

17. Úplný zdrojový kód příkladu pro parsing HTML stránky

18. Získávání informací z HTML stránek zajímavější cestou: knihovna Beautiful Soup

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Zpracování XML a HTML v Pythonu s využitím knihovny lxml

Knihovna lxml, s jejímiž základy se dnes seznámíme, slouží k načítání (parsování) XML souborů, 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 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 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.

Poznámka: podobným směrem se vydala i známá a velmi populární knihovna requests, která někdy poměrně složité mechanismy obaluje programátorským rozhraním, jež se používá velmi snadno. S touto knihovnou jste se mohli seznámit na konkurenčním serveru [1]. [2].

2. Instalace knihovny lxml

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

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

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

$ pip3 list | grep lxml
lxml (3.3.3)
Poznámka: mimochodem – verze 3.3.3 zobrazená na výpisu nahoře je již dnes zastará, takže by se měl provést update na verzi 4.x.x (nejnovější stabilní verze je v současnosti verze 4.2.5):
$ sudo pip3 install lxml -U
Collecting lxml
  Downloading https://files.pythonhosted.org/packages/03/a4/9eea8035fc7c7670e5eab97f34ff2ef0ddd78a491bf96df5accedb0e63f5/lxml-4.2.5-cp36-cp36m-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.2.5

Nyní znovu zkontrolujeme verzi nainstalované knihovny:

$ pip3 show lxml
 
Name: lxml
Version: 4.2.5
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.6/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.2.5-cp27-cp27mu-manylinux1_x86_64.whl (5.8MB)
    100% |████████████████████████████████| 5.8MB 89kB/s
Installing collected packages: lxml
Successfully installed lxml-4.2.5

3. Použití objektů typu Element a ElementTree

Nejprve se podívejme na způsob vytváření souborů XML. Vzhledem k tomu, že se ve skutečnosti jedná o zápis stromové struktury (s jediným kořenovým uzlem, každý uzel/element kromě kořenového má jediného předka, neexistují žádné cykly), pravděpodobně nás nepřekvapí, že se XML skutečně konstruuje s využitím objektů Element a ElementTree.

Objekt typu Element je možné považovat za datovou strukturu, jejíž základní vlastnosti jsou převzaté z klasických seznamů a současně i slovníků (viz další seznam). Instance třídy Element představuje jeden uzel stromu a může obsahovat celou řadu vlastností (properties), především pak:

  • Značku (tag), což je řetězec se jménem elementu (uzlu).
  • Atributy přiřazené k elementu. Interně se jedná o slovník s dvojicemi jméno atributu:hodnota atributu. Počet atributů je libovolný, jejich jméno jsou samozřejmě unikátní.
  • Text (hodnota) ve značce. Může být prázdný. Viz navazující kapitoly.
  • Všechny přímé potomky (children). Interně se jedná o sekvenci a opět platí, že koncový uzel (list) nemá žádné potomky.

Existuje několik způsobů, jak zavolat konstruktor třídy Element. V tom nejjednodušším případě se konstruktoru předá pouze značka (jméno) elementu ve formě řetězce:

import lxml.etree as ET
 
root = ET.Element("root")

Potomky jakéhokoli elementu můžeme vytvořit s využitím SubElement. Jedná se o tovární funkci, které se předává reference na předka (dříve vytvořený uzel/element) a taktéž značka (jméno) nového elementu. K jedinému uzlu, který jsme vytvořili předchozím příkazem, tedy přidáme dva potomky následujícím způsobem:

left = ET.SubElement(root, "left")
right = ET.SubElement(root, "right")

V dalším kroku již můžeme vytvořit instanci třídy ElementTree, která celou hierarchii uzlů zabaluje. Konstruktoru této třídy se předává kořenový uzel:

# konstrukce stromu
tree = ET.ElementTree(root)

Tato třída nabízí uživatelům celou řadu užitečných metod. Například se jedná o metodu nazvanou find sloužící k nalezení určitého uzlu na základě zadané cesty; dále pak o metody pojmenované findall a findnext, popř. o metodu write, kterou je možné použít pro uložení stromu do souboru (v XML podobě).

4. Uložení vytvořeného XML v komprimované i čitelné podobě

K poslední metodě, o níž jsme se zmínili na konci předchozí kapitoly, se na chvíli vraťme. Tuto metodu totiž použijeme ve chvíli, kdy potřebujeme vytvořit výsledný XML soubor. Metodě se předává buď pouze název souboru, nebo i další nepovinné (pojmenované) parametry. Nejjednodušší způsob použití vypadá následovně:

tree.write("test1.xml")

Nepovinným parametrem pretty_print si můžeme (alespoň částečně) vynutit vygenerování XML souboru určeného pro čtení uživateli. Poduzly jsou v takovém případě odsazeny od levého okraje, ovšem pouze v případě, že neobsahují žádný text (což je v našem případě splněno):

tree.write("test1_pretty_print.xml", pretty_print=True)

Podívejme se nyní na první demonstrační příklad, v němž vytvoříme jednoduchý strom s kořenem a dvěma poduzly. Tento strom následně uložíme do dvou XML souborů – bez formátování a s formátováním:

import lxml.etree as ET
 
root = ET.Element("root")
 
left = ET.SubElement(root, "left")
right = ET.SubElement(root, "right")
 
# konstrukce stromu
tree = ET.ElementTree(root)
 
# zapis do souboru
tree.write("test1.xml")
 
tree.write("test1_pretty_print.xml", pretty_print=True)

První XML soubor je vygenerován takovým způsobem, aby byl co nejkratší:

<root><left/><right/></root>

Druhý XML soubor naopak poduzly odsazuje:

<root>
  <left/>
  <right/>
</root>
Poznámka: povšimněte si, že prázdné uzly jsou ve výchozím nastavení zapisovány stylem <značka/>, tj. neuvádí se zbytečně dlouhý zápis <značka></značka>. Dále neobsahují výsledné XML definici verze XML ani určení kódování. Tyto dva prvky je možné do výsledného souboru v případě potřeby přidat.

5. Vložení obsahu (textu) do značek

Zkusme si nyní vytvořit nepatrně složitější XML soubor, v němž budou poduzly obsahovat nějaký text. Ten může být libovolný, tj. můžeme použít například klasické ASCII znaky (univerzálně podporovaná podmnožina Unicode):

root = ET.Element("root")
root.text = "any text"

Ovšem nic nám nebrání použít i další znaky Unicode:

left = ET.SubElement(root, "left")
left.text = "ěščř"

Dokonce můžeme použít i znaky, které mají v XML speciální význam:

middle = ET.SubElement(root, "middle")
middle.text = "<&>"

A samozřejmě je možné použít i prázdný text:

right = ET.SubElement(root, "right")
right.text = ""

XML soubory s takovými uzly vytvoříme v dnešním druhém demonstračním příkladu:

import lxml.etree as ET
 
root = ET.Element("root")
root.text = "any text"
 
left = ET.SubElement(root, "left")
left.text = "ěščř"
 
middle = ET.SubElement(root, "middle")
middle.text = "<&>"
 
right = ET.SubElement(root, "right")
right.text = ""
 
# konstrukce stromu
tree = ET.ElementTree(root)
 
# zapis do souboru
tree.write("test2.xml")
 
tree.write("test2_pretty_print.xml", pretty_print=True)

Výsledek bude vypadat následovně:

<root>any text<left>ěščř</left><middle><&></middle><right></right></root>
Poznámka: znaky mimo základní sadu ASCII jsou reprezentovány svým kódem, takže za cenu menší čitelnosti získáme soubor, který bude zpracovatelný i ASCII parserem (pokud takový parser pro XML existuje).

Vzhledem k tomu, že uzly obsahují text, bude výsledek neformátovaný i v případě, že použijeme parametr pretty_print:

<root>any text<left>ěščř</left><middle><&></middle><right></right></root>

Nyní si zkusme ověřit, co se stane ve chvíli, kdy do textového uzlu přidáme text s konci řádků:

import lxml.etree as ET
 
root = ET.Element("root")
root.text = "text, který může\nobsahovat i konce řádků"
 
left = ET.SubElement(root, "left")
left.text = "ěščř"
 
right = ET.SubElement(root, "right")
right.text = "\n\n\n"
 
# konstrukce stromu
tree = ET.ElementTree(root)
 
# zapis do souboru
tree.write("test3.xml")
 
tree.write("test3_pretty_print.xml", pretty_print=True)

Konce řádků se přesně objeví i ve výsledném XML:

<root>text, který může
obsahovat i konce řádků<left>ěščř</left><right>
 
 
</right></root>

Vzhledem k tomu, že uzly obsahují text, bude výsledek neformátovaný i v případě, že použijeme parametr pretty_print:

<root>text, který může
obsahovat i konce řádků<left>ěščř</left><right>
 
 
</right></root>

6. Nepatrně složitější strom se třemi úrovněmi

Další demonstrační příklad je uveden pouze pro úplnost a pro ukázku nepatrně složitějšího XML. Vytvoříme v něm stromovou strukturu se třemi úrovněmi. Na první úrovni se samozřejmě nachází kořenový uzel. Na úrovni druhé jsou tři uzly (levý, prostřední a pravý) a každý z těchto uzlů obsahuje tři potomky (listy):

import lxml.etree as ET
 
root = ET.Element("root")
 
left1stLevel = ET.SubElement(root, "left")
middle1stLevel = ET.SubElement(root, "middle")
right1stLevel = ET.SubElement(root, "right")
 
left2ndLevelA = ET.SubElement(left1stLevel, "left")
middle2ndLevelA = ET.SubElement(left1stLevel, "middle")
right2ndLevelA = ET.SubElement(left1stLevel, "right")
 
left2ndLevelB = ET.SubElement(middle1stLevel, "left")
middle2ndLevelB = ET.SubElement(middle1stLevel, "middle")
right2ndLevelB = ET.SubElement(middle1stLevel, "right")
 
left2ndLevelC = ET.SubElement(right1stLevel, "left")
middle2ndLevelC = ET.SubElement(right1stLevel, "middle")
right2ndLevelC = ET.SubElement(right1stLevel, "right")
 
# konstrukce stromu
tree = ET.ElementTree(root)
 
# zapis do souboru
tree.write("test4.xml")
 
tree.write("test4_pretty_print.xml", pretty_print=True)

Výsledné XML zapsané ve zhuštěné podobě na jediném řádku:

<root><left><left/><middle/><right/></left><middle><left/><middle/><right/></middle><right><left/><middle/><right/></right></root>

V případě použití parametru pretty_print je ze struktury a odsazení uzlů jasně patrná původní stromová struktura, kterou jsme v příkladu vytvořili:

<root>
  <left>
    <left/>
    <middle/>
    <right/>
  </left>
  <middle>
    <left/>
    <middle/>
    <right/>
  </middle>
  <right>
    <left/>
    <middle/>
    <right/>
  </right>
</root>

7. Specifikace atributů jednotlivých uzlů ve stromu

Již z předchozího popisu víme, že uzel může kromě textu (umístěného uvnitř jeho značky) obsahovat i libovolný počet atributů, přičemž každý atribut má své jméno a hodnotu. Jména atributů by měla být unikátní a mělo by se jednat o platné identifikátory odpovídající této gramatice:

NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF]
NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]

Na hodnotu atributu se toto omezení nevztahuje, pouze je zapotřebí řešit kódování uvozovek, úhlových závorek atd. To za nás provede knihovna lxml automaticky.

V následujícím příkladu opět vytvoříme strom se třemi úrovněmi, nyní však budou mít jednotlivé uzly přiřazené atributy (všechny budou mít atribut „popis“):

import lxml.etree as ET
 
root = ET.Element("root", atribut1="1", attribut2="2", popis="koren")
 
left1stLevel = ET.SubElement(root, "left", popis="levy vnitrni poduzel")
middle1stLevel = ET.SubElement(root, "middle", popis="prostredni vnitrni poduzel")
right1stLevel = ET.SubElement(root, "right", popis="pravy vnitrni poduzel")
 
left2ndLevelA = ET.SubElement(left1stLevel, "left", popis="list zcela nalevo")
middle2ndLevelA = ET.SubElement(left1stLevel, "middle", popis="list")
right2ndLevelA = ET.SubElement(left1stLevel, "right", popis="list")
 
left2ndLevelB = ET.SubElement(middle1stLevel, "left", popis="list")
middle2ndLevelB = ET.SubElement(middle1stLevel, "middle", popis="prostredni list")
right2ndLevelB = ET.SubElement(middle1stLevel, "right", popis="list")
 
left2ndLevelC = ET.SubElement(right1stLevel, "left", popis="list")
middle2ndLevelC = ET.SubElement(right1stLevel, "middle", popis="list")
right2ndLevelC = ET.SubElement(right1stLevel, "right", popis="list zcela napravo")
 
# konstrukce stromu
tree = ET.ElementTree(root)
 
# zapis do souboru
tree.write("test5.xml")
 
tree.write("test5_pretty_print.xml", pretty_print=True)

Výsledné XML zapsané ve zhuštěné podobě na jediném řádku:

<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>

V případě použití parametru pretty_print je ze struktury a odsazení uzlů jasně patrná původní stromová struktura, kterou jsme v příkladu vytvořili:

<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>

8. Parsování XML s využitím knihovny lxml

Ve druhé části článku si ukážeme základy parsování XML, samozřejmě opět s využitím knihovny lxml. V následujícím příkladu využijeme soubor „test5.xml“, který vznikl spuštěním demonstračního příkladu ze sedmé kapitoly. Tento soubor – který neobsahuje specifikaci verze XML, kódování, o schématu ani nemluvě atd. – dokáže knihovna lxml bez problémů načíst a zparsovat. 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>'

9. 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

10. Využití cesty (path) při přístupu k uzlům

V některých jednodušších případech si sice vystačíme se zpracováním stromu pomocí metody getchildren, ovšem knihovna lxml nám dává k dispozici mnohem silnější technologii nazvanou XPath neboli „XML Path Language“. Jedná se o doménově specifický jazyk (DSL), který primárně slouží k přístupu k elementům stromové struktury na základě absolutně nebo relativně zadané cesty. Možnosti nabízené XPathem jsou však větší; s některými z nich se seznámíme příště. Nyní si ukažme pravděpodobně nejjednodušší příklad, v němž použijeme cestu přesně popisující jediný uzel ve stromu. Cesta je popsána jmény značek (což je v našem stromu zcela jednoznačné, podobně jako můžeme popsat cestu k souboru v adresářové struktuře):

/root/right/right

Příklad získá všechny uzly odpovídající zadané cestě a vypíše popisy těchto uzlů:

import lxml.etree as ET
 
xml = "test5.xml"
tree = ET.parse(xml)
 
# absolutni cesta
nodes = tree.xpath("/root/right/right")
 
print("Nodes: {}".format(len(nodes)))
 
for node in nodes:
    print(node.get("popis"))

Vzhledem k tomu, že cesta přesně specifikuje jediný uzel/element, bude výpis vypadat takto:

Nodes: 1
list zcela napravo

Můžeme ovšem postupovat i jinak a metodu xpath volat přímo na kořenovém elementu a nikoli nad celým stromem. Zbytek příkladu bude stejný a stejný bude v tomto případě i výsledek:

import lxml.etree as ET
 
xml = "test5.xml"
tree = ET.parse(xml)
 
root = tree.getroot()
 
# absolutni cesta
nodes = root.xpath("/root/right/right")
 
print("Nodes: {}".format(len(nodes)))
 
for node in nodes:
    print(node.get("popis"))

S výsledkem:

Nodes: 1
list zcela napravo

11. Použití relativních cest

Cesta může být zadána i relativně, tj. bez úvodního lomítka. Vzhledem k tomu, že metodu xpath opět voláme nad kořenovým uzlem, bude výsledek stejný, protože relativní cesta se bude vztahovat k tomuto uzlu:

import lxml.etree as ET
 
xml = "test5.xml"
tree = ET.parse(xml)
 
root = tree.getroot()
 
# relativni cesta
nodes = root.xpath("right/right")
 
print("Nodes: {}".format(len(nodes)))
 
for node in nodes:
    print(node.get("popis"))

S výsledkem:

Nodes: 1
list zcela napravo

12. Přístup k hodnotám uzlů s využitím XPath

Možnosti doménově specifického jazyka XPath jsou ve skutečnosti mnohem větší. Pro zajímavost si ukažme, jak můžeme získat text (hodnotu) libovolného uzlu/elementu přímo specifikací cesty:

import lxml.etree as ET
 
xml = "test2.xml"
tree = ET.parse(xml)
 
root = tree.getroot()
 
text = root.xpath("//text()")
print(text)
 
text = root.xpath("left//text()")
print(text)
 
text = tree.xpath("/root/left//text()")
print(text)

Povšimněte si, že v prvním případě se vrátí text všech nalezených uzlů, ve druhém případě text z uzlu „left“ (je použita relativní cesta) a v případě posledním textu taktéž z uzlu „left“, ovšem specifikovaného absolutní cestou:

['any text', 'ěščř', '<&>']
['ěščř']
['ěščř']
Poznámka: toto je stále velmi jednoduchý až primitivní příklad. Cesty totiž mohou být zadány mnohem obecněji, například můžeme specifikovat, že se mezi kořenovým uzlem a hledaným uzlem může nacházet složitější hierarchie (více úrovní), při vyhledání se mohou používat různé typy testů atd. Složitější a nutno říci že i praktičtější příklady si uvedeme příště.

13. Získání cesty k vybranému uzlu pro pozdější použití

Při konstrukci stromu popř. při analýze naparsovaného XML může být užitečné zjistit, jak by vlastně mohla vypadat cesta k nějakém uzlu. I tuto možnost knihovna lxml nabízí, protože pro libovolný uzel (i uzel kořenový) je možné zavolat metodu getpath, která vrátí absolutní cestu k danému uzlu/elementu:

import lxml.etree as ET
 
root = ET.Element("root", atribut1="1", attribut2="2", popis="koren")
 
left1stLevel = ET.SubElement(root, "left", popis="levy vnitrni poduzel")
right1stLevel = ET.SubElement(root, "right", popis="pravy vnitrni poduzel")
 
left2ndLevelA = ET.SubElement(left1stLevel, "left", popis="list zcela nalevo")
right2ndLevelA = ET.SubElement(left1stLevel, "right", popis="list")
 
left2ndLevelB = ET.SubElement(right1stLevel, "left", popis="list")
right2ndLevelB = ET.SubElement(right1stLevel, "right", popis="list zcela napravo")
 
# konstrukce stromu
tree = ET.ElementTree(root)
 
print(tree.getpath(root))
print(tree.getpath(left1stLevel))
print(tree.getpath(right2ndLevelB))

Výsledkem je trojice absolutních cest:

/root
/root/left
/root/right/right
Poznámka: ve skutečnosti je ovšem situace komplikovanější, protože poduzly na stejné úrovni mohou mít shodné značky. Sami si ostatně vyzkoušejte, jak v takovém případě budou cesty fungovat a zda budou jednoznačné.

14. Základy zpracování HTML stránek

Knihovnu lxml je možné v případě potřeby použít i pro zpracování HTML stránek. Při zpracovávání HTML stránek se nevyžaduje (a popravdě řečeno ani neočekává) validita stránky, takže se specializovaný parser nazvaný HTMLParser snaží z dodaného zdrojového kódu stránky získat korektní stromovou strukturu, a to i v případě, že autor například neuzavírá značky, nevkládá uzavírací značky ve správném pořadí atd. Podívejme se nyní na jednoduchý příklad zpracovatelné HTML stránky, konkrétně na stránku dostupnou na adrese http://www.zyvra.org/html/simple.htm. Zdrojový kód této stránky vypadá následovně (stylem zápisu trošku připomíná minulé tisíciletí :-):

<html><head><title>
Very simple HTML page.
</title></head>
<body>
 
<p>You can look at the source of this page by: Right clicking anywhere
out in space on this page then selecting "View" in the menu.</p>
<p>This works on any page, but sometimes
what you see may be very complex
and seem confusing.</p>
 
<p>
<b>Please,</b> look at the source and what you see with the browser.
You should understand and see the effect of every tag. Use the little
Icons up in the right of your browser screen to change the size of the
window and see the effect, and how the browser displays this page.</p>
<p align="right">
Yes, this is a <b>Very Plain</b> page. <i>But it works!</i></p>
<p><i><b>Remember. We are just getting started,</b> and I haven't used
anything more than I have talked about in a couple pages!</i>
Yes. You will want to be more fancy.
Just be patient, we'll get there.
 
<p align="center">Now create a page like
this of your own. <b>Have fun!</b></p
 
</body></html>

Povšimněte si například toho, že se ve stránce objevují neuzavřené značky <p>, chybí informace o verzi HTML, o kódování atd. I přesto je možné takovou stránku zpracovat, což je ukázáno v následující kapitole.

15. Použití objektu typu HTMLParser

Realizace příkladu pro zpracování HTML stránky je poměrně přímočará. Nejprve (přesněji řečeno po importu lxml.etree) vytvoříme instanci parseru HTML stránek:

import lxml.etree as ET
 
parser = ET.HTMLParser()

Dále se pokusíme stránku načíst a ihned poté zparsovat. Povšimněte si, že funkce lxml.etree.parse rozpozná URL a stránku v případě potřeby stáhne (pokud ovšem není dostupná na serveru s HTTPS!):

url = "http://www.zyvra.org/html/simple.htm"
 
tree = ET.parse(url, parser)
 
root = tree.getroot()

Nyní, když máme k dispozici celý strom i kořenový prvek, se můžeme pokusit zpětně zrekonstruovat řetězec se zdrojovým kódem stránky:

print("\n\n\nContent:")
result = ET.tostring(tree.getroot(), pretty_print=True, method="html")
print(result)

S výsledkem:

Content:
b'<html>\n<head><title>\nVery simple HTML page.\n</title></head>\n<body>\n\n<p>You can look at the source of this page by: Right clicking anywhere\nout in space on this page then selecting "View" in the menu.</p>\n<p>This works on any page, but sometimes\nwhat you see may be very complex\nand seem confusing.</p>\n\n<p>\n<b>Please,</b> look at the source and what you see with the browser.\nYou should understand and see the effect of every tag. Use the little\nIcons up in the right of your browser screen to change the size of the\nwindow and see the effect, and how the browser displays this page.</p>\n<p align="right">\nYes, this is a <b>Very Plain</b> page. <i>But it works!</i></p>\n<p><i><b>Remember. We are just getting started,</b> and I haven\'t used\nanything more than I have talked about in a couple pages!</i>\nYes. You will want to be more fancy.\nJust be patient, we\'ll get there.\n\n</p>\n<p align="center">Now create a page like\nthis of your own. <b>Have fun!</b></p>\n</body>\n</html>\n'
Poznámka: v tomto případě neměla volba pretty_print žádoucí účinek, protože jednotlivé elementy (značky) obsahují text.

Popř. získat text titulku:

print("\n\n\nTitle:")
title = root.xpath("/html/head/title/text()")
print(title[0])

S výsledky:

Title:
 
Very simple HTML page.
Poznámka: zde se předpokládá, že titulek bude na stránce zadán pouze jedenkrát. Korektnější by samozřejmě byla kontrola, zda title není prázdným seznamem.

16. Přečtení obsahu všech odstavců, získání atributů prvků atd.

Dále můžeme získat všechny odstavce uzavřené do značky <p>, a to opět použitím metody xpath:

divs = root.xpath("body//p")

Vypsat můžeme textový obsah všech nalezených odstavců (obsah může být prázdný v případě nepárové značky):

print("\n\n\nText:")
 
for div in divs:
    print(div.text)

S výsledky:

Text:
You can look at the source of this page by: Right clicking anywhere
out in space on this page then selecting "View" in the menu.
This works on any page, but sometimes
what you see may be very complex
and seem confusing.
 
 
 
 
Yes, this is a
None
Now create a page like
this of your own.

Samozřejmě nám nikdo nebrání získat informace o libovolném atributu, například o atributu „class“, „style“ či „align“:

print("\n\n\nAlign:")
 
for div in divs:
    print(div.get("align"))

S výsledky:

Align:
None
None
None
right
None
center

17. Úplný zdrojový kód příkladu pro parsing HTML stránky

Úplný zdrojový kód tohoto demonstračního příkladu vypadá následovně:

import lxml.etree as ET
 
parser = ET.HTMLParser()
 
url = "http://www.zyvra.org/html/simple.htm"
 
tree   = ET.parse(url, parser)
 
root = tree.getroot()
 
print("\n\n\nContent:")
result = ET.tostring(tree.getroot(), pretty_print=True, method="html")
print(result)
 
print("\n\n\nTitle:")
title = root.xpath("/html/head/title/text()")
print(title[0])
 
divs = root.xpath("body//p")
 
print("\n\n\nText:")
 
for div in divs:
    print(div.text)
 
print("\n\n\nAlign:")
 
for div in divs:
    print(div.get("align"))

18. Získávání informací z HTML stránek zajímavější cestou: knihovna Beautiful Soup

Pokud budete potřebovat vytvořit aplikaci získávající data ze složitěji strukturovaných HTML stránek, může být lepší namísto dnes popisované knihovny lxml použít další zajímavou a užitečnou knihovnu nazvanou Beautiful Soup. S možnostmi nabízenými touto knihovnou se seznámíme v samostatném článku, v němž si samozřejmě ukážeme i různé příklady (tentokrát již převzaté z praxe).

19. Repositář s demonstračními příklady

Všechny dnes popisované demonstrační příklady byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/lxml-examples. Příklady si můžete v případě potřeby stáhnout i jednotlivě bez nutnosti klonovat celý repositář:

# Příklad Popis Odkaz
1 elementtree1.py použití objektů Element a SubElement https://github.com/tisnik/lxml-examples/blob/master/elementtree1.py
2 elementtree2.py nastavení textu u elementů/prvků https://github.com/tisnik/lxml-examples/blob/master/elementtree2.py
3 elementtree3.py texty s koncem řádku a speciálními znaky https://github.com/tisnik/lxml-examples/blob/master/elementtree3.py
4 elementtree4.py tvorba XML tvořeného stromem se třemi úrovněmi https://github.com/tisnik/lxml-examples/blob/master/elementtree4.py
5 elementtree5.py elementy obsahující atributy https://github.com/tisnik/lxml-examples/blob/master/elementtree5.py
       
6 parsing1.py základ parseru XML https://github.com/tisnik/lxml-examples/blob/master/parsing1.py
7 parsing2.py výpis všech potomků kořenového uzlu https://github.com/tisnik/lxml-examples/blob/master/parsing2.py
8 parsing3.py využití xpath https://github.com/tisnik/lxml-examples/blob/master/parsing3.py
9 parsing4.py využití xpath https://github.com/tisnik/lxml-examples/blob/master/parsing4.py
10 parsing5.py relativní cesta https://github.com/tisnik/lxml-examples/blob/master/parsing5.py
11 parsing6.py čtení textu elementů přes cestu https://github.com/tisnik/lxml-examples/blob/master/parsing6.py
       
12 get_path.py získání cesty k vybranému elementu (elementům) https://github.com/tisnik/lxml-examples/blob/master/get_path.py
       
13 parsing_html.py parsing HTML stránky https://github.com/tisnik/lxml-examples/blob/master/parsing_html.py
Poznámka: v repositáři jsou uloženy i výsledky běhu jednotlivých příkladů, tj. vytvořené XML soubory popř. naopak výsledek jejich parsování.

20. Odkazy na Internetu

  1. lxml – XML and HTML with Python
    https://lxml.de/index.html
  2. lxml na PyPi
    https://pypi.org/project/lxml/
  3. ElementTree and lxml
    https://wiki.python.org/mo­in/Tutorials%20on%20XML%20pro­cessing%20with%20Python
  4. ElementTree Overview
    http://effbot.org/zone/element-index.htm
  5. Elements and Element Trees
    http://effbot.org/zone/element.htm
  6. Python XML processing with lxml
    http://infohost.nmt.edu/tcc/hel­p/pubs/pylxml/web/index.html
  7. Dive into Python 3: XML
    http://www.diveintopython3­.net/xml.html
  8. Programovací jazyk Clojure – základy zpracování XML
    https://www.root.cz/clanky/pro­gramovaci-jazyk-clojure-zaklady-zpracovani-xml/
  9. xml-zip
    http://clojuredocs.org/clojure.zip/xml-zip
  10. xml-seq
    http://clojuredocs.org/clo­jure.core/xml-seq
  11. Parsing XML in Clojure
    https://www.root.cz/clanky/pro­gramovaci-jazyk-clojure-zaklady-zpracovani-xml/
  12. Tree structure
    https://en.wikipedia.org/wi­ki/Tree_structure
  13. Strom (datová struktura)
    https://cs.wikipedia.org/wi­ki/Strom_(datov%C3%A1_struk­tura)
  14. Element Library Functions
    http://effbot.org/zone/element-lib.htm#prettyprint
  15. The XML C parser and toolkit of Gnome
    http://xmlsoft.org/
  16. XML Tutorial na zvon.org
    http://www.zvon.org/comp/r/tut-XML.html
  17. Extensible Markup Language (XML) 1.0 (Fifth Edition)
    https://www.w3.org/TR/REC-xml/
  18. XML Processing Modules (pro Python)
    https://docs.python.org/3/li­brary/xml.html
  19. Užitečné knihovny a moduly pro Python: knihovna Requests
    https://mojefedora.cz/uzitecne-knihovny-pro-python-requests-1/
  20. Užitečné knihovny a moduly pro Python: další možnosti nabízené knihovnou Requests
    https://mojefedora.cz/uzitecne-knihovny-a-moduly-pro-python-dalsi-moznosti-nabizene-knihovnou-requests/
  21. Extensible Markup Language
    https://en.wikipedia.org/wiki/XML
  22. Extensible Markup Language
    https://cs.wikipedia.org/wi­ki/Extensible_Markup_Langu­age
  23. Slabikář XML – odkazy
    https://www.interval.cz/clan­ky/slabikar-xml-odkazy/
  24. XML editors
    http://www.xml-dev.com/
  25. lxml FAQ – Frequently Asked Questions
    https://lxml.de/FAQ.html
  26. XML pro začátečníky – 1. část
    http://programujte.com/cla­nek/2007030501-xml-pro-zacatecniky-1-cast/
  27. 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
  28. XML Schema
    https://cs.wikipedia.org/wi­ki/XML_Schema
  29. Meaning of – <?xml version=“1.0” encoding=“utf-8”?>
    https://stackoverflow.com/qu­estions/13743250/meaning-of-xml-version-1–0-encoding-utf-8#27398439
  30. Beautiful Soup
    https://www.crummy.com/sof­tware/BeautifulSoup/
Našli jste v článku chybu?