Zpracování XML a HTML v Pythonu s využitím knihoven lxml a Beautiful Soup

11. 10. 2018
Doba čtení: 28 minut

Sdílet

Dnes si nejprve ukážeme některé další možnosti nabízené knihovnou lxml, která je určena pro načítání, modifikaci i ukládání souborů ve formátech XML a HTML. Pak se seznámíme s knihovnou se záhadným názvem Beautiful Soup.

Obsah

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

2. Soubory POM (Project Object Model)

3. První demonstrační příklad – načtení XML souboru pom.xml

4. Použití XPath při přístupu ke konkrétnímu uzlu

5. Získání a výpis informací o modulech, na nichž závisí projekt

6. Podmínky použité při výběru uzlů pomocí cesty

7. Porovnání hodnoty uzlu s textovým řetězcem

8. Složitější cesta s relativní částí

9. Knihovna Beautiful Soup

10. Získání základních informací z HTML stránky

11. Zjednodušení předchozího příkladu pomocí tečkového operátoru

12. Nalezení všech značek <title> a výpis jejich obsahu

13. Výpis všech odkazů ve značkách <a>

14. Složitější příklad – získání klíčových slov přiřazených k balíčkům

15. Další zpracování klíčových slov přiřazených k balíčkům

16. Získání tučného textu z HTML stránky

17. Přečtení obsahu tabulek z HTML stránky

18. Úplný zdrojový kód předchozího demonstračního příkladu

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

20. Odkazy na Internetu

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

V úvodním článku o knihovně xml jsme se seznámili se základními koncepty, na nichž je tato užitečná knihovna postavena. Připomeňme si, že pro reprezentaci dokumentu načítaného či ukládaného do XML jsou použity objekty typu 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ů. Instance třídy Element představuje jeden uzel vytvářeného či naparsovaného stromu dokumentu a může obsahovat celou řadu vlastností (properties), zejména pak samotnou značku, atributy, text (hodnotu umístěnou ve značce) a získat lze i reference na všechny přímé potomky uzlu. Naproti tomu třída ElementTree celý strom „obaluje“ a nabízí uživatelům další užitečné metody pro hledání uzlů, zápis stromu do XML apod.

Taktéž jsme si řekli základní informace o použití technologie XPath při vyhledávání uzlů na základě nějakých kritérií. Na tuto zajímavou část v dnešním článku částečně navážeme, protože si ukážeme některé další možnosti, které nám knihovna lxml v této oblasti nabízí.

Ovšem použití knihovny lxml nemusí být vždy to nejlepší řešení, i když zde nalezneme například specializovaný parser nazvaný příznačně HTMLParser. V případě, že je zapotřebí získávat informace z HTML stránek (web scraping), může být výhodnější použít knihovnu specializovanou právě na tuto oblast. Jednou z dostupných a často používaných knihoven pro web scraping a podobné činnosti je knihovna s poněkud záhadným názvem Beautiful Soup. I s některými možnostmi nabízenými touto knihovnou se dnes alespoň ve stručnosti seznámíme.

2. Soubory POM (Project Object Model)

Zejména vývojáři pracující s programovacím jazykem Java a systémem Maven se určitě již mnohokrát setkali se soubory nazvanými pom.xml. Jen ve stručnosti si řekněme, že tyto soubory obsahují informace o projektu a samozřejmě i konfiguraci celého projektu, popř. další informace používané některými Maven pluginy. Tyto soubory jsou primárně využívané nástrojem Apache Maven, přičemž zkratka POM znamená Project Object Model. Ve skutečnosti ovšem s těmito soubory pracuje i mnoho dalších nástrojů, ať již se jedná o integrovaná vývojová prostředí či o specializovanější nástroje určené pro kontrolu závislých knihoven, licencí použitých v knihovnách atd. atd.

Velmi jednoduchý soubor pom.xml může vypadat například následovně:

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.tisnik.uberproject.test</groupId>
  <artifactId>test-app-junit-dependency</artifactId>
  <version>1.0</version>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
    </dependency>
    <dependency>
      <groupId>foo</groupId>
      <artifactId>foo</artifactId>
      <version>1.0.0</version>
    </dependency>
    <dependency>
      <groupId>bar</groupId>
      <artifactId>bar</artifactId>
      <version>1.2.3</version>
    </dependency>
  </dependencies>
</project>
Poznámka: tento soubor, který budeme používat v demonstračních příkladech, naleznete na GitHubu, konkrétně na adrese https://github.com/tisnik/lxml-examples/blob/master/pom.xml.

3. První demonstrační příklad – načtení XML souboru pom.xml

Vzhledem k tomu, že soubory pom.xml jsou klasickými XML soubory, můžeme pro jejich načtení a parsing použít funkci lxml.etree.parse(), s níž jsme se již seznámili minule. Této funkci předáme jméno souboru. V případě, že se dokument podaří načíst (a to by neměl být problém, samozřejmě za předpokladu, že pom.xml nebyl nějak poškozen), lze získat kořenový uzel, rekurzivně vytisknout jeho obsah (tedy celý strom), přečíst přímé potomky kořenového uzlu a následně vypsat (například) jejich značky:

import lxml.etree as ET
 
xml = "pom.xml"
tree = ET.parse(xml)
 
root = tree.getroot()
print(ET.tostring(root))
print()
 
children = root.getchildren()
 
for child in children:
    print(child.tag)

Výsledek by pro náš pom.xml měl vypadat následovně. První část s rekurzivním výpisem celého stromu není příliš čitelná, ovšem to nevadí, protože vždy můžeme použít pretty printing:

b'<project>\n  <modelVersion>4.0.0</modelVersion>\n
<groupId>org.tisnik.uberproject.test</groupId>\n
<artifactId>test-app-junit-dependency</artifactId>\n
<version>1.0</version>\n  <dependencies>\n
<dependency>\n      <groupId>junit</groupId>\n
<artifactId>junit</artifactId>\n
<version>3.8.1</version>\n    </dependency>\n
<dependency>\n      <groupId>foo</groupId>\n
<artifactId>foo</artifactId>\n
<version>1.0.0</version>\n    </dependency>\n
<dependency>\n      <groupId>bar</groupId>\n
<artifactId>bar</artifactId>\n
<version>1.2.3</version>\n    </dependency>\n
</dependencies>\n</project>'
 
modelVersion
groupId
artifactId
version
dependencies
Poznámka: na tomto místě je nutné poznamenat, že struktura souborů pom.xml není tak rigidní a jednoduchá, jak by se z předchozího demonstračního příkladu mohlo zdát. V praxi tedy bude zpracování XML nepatrně složitější. Ostatně se stačí podívat na „Super POM“, jehož úplný výpis naleznete na adrese https://maven.apache.org/gu­ides/introduction/introduc­tion-to-the-pom.html#Super_POM. Nicméně pro účely ukázky možností knihovny lxml nám bude postačovat jednodušší příklad.

4. Použití XPath při přístupu ke konkrétnímu uzlu

Pro přístup ke konkrétnímu uzlu se většinou využívá technologie pojmenovaná XPath, s jejímiž základy jsme se seznámili minule. Podívejme se nyní na příklad použití xpathu v případě, že potřebujeme získat základní informace o projektu popsaného v souboru pom.xml. Mezi základní informace patří mj. i artifactId, groupId a version, přesněji řečeno obsah těchto tří uzlů (nikoli ovšem jejich atributy). Příslušné (absolutní) cesty budou vypadat takto:

/project/artifactId/text()
/project/groupId/text()
/project/version/text()

Připomeňme si, že pomocí „text()“ dokážeme přečíst textovou hodnotu zapsanou v uzlu/elementu.

Následuje výpis úplného zdrojového kódu demonstračního příkladu, který tyto cesty používá pro získání informací o projektu. Díky poměrně velké jistotě, že se tyto informace budou v pom.xml nacházet, použijeme přístup k prvnímu prvku vráceného seznamu přímo s využitím indexu:

import lxml.etree as ET
 
xml = "pom.xml"
tree = ET.parse(xml)
 
root = tree.getroot()
 
artifact_id = tree.xpath("/project/artifactId/text()")[0]
print("artifact ID: {aid}".format(aid=artifact_id))
 
group_id = tree.xpath("/project/groupId/text()")[0]
print("Group ID: {gid}".format(gid=group_id))
 
version = tree.xpath("/project/version/text()")[0]
print("Version: {v}".format(v=version))

Výsledek by měl na standardním výstupu vypadat následovně:

Artifact ID: test-app-junit-dependency
Group ID: org.tisnik.uberproject.test
Version: 1.0
Poznámka: bližší informace o základních vlastnostech projektu získáte například na stránce Guide to naming conventions on groupId, artifactId, and version.

5. Získání a výpis informací o modulech, na nichž závisí projekt

Ve chvíli, kdy budeme potřebovat z našeho projektového souboru uloženého ve formátu XML získat všechny moduly, na nichž projekt přímo závisí, můžeme použít cestu (xpath), která bude vybírat všechny uzly nazvané „dependency“, které se nachází v poduzlu „dependencies“. Prozatím použijeme jednoduchou absolutní cestu, která bude vypadat následovně:

/project/dependencies/dependency

Pro každý nalezený uzel (či lépe řečeno element) pak přečteme text z poduzlu nazvaného „groupId“, a to opět s využitím cesty:

for dependency in dependencies:
    print(dependency.xpath("groupId/text()")[0])

Úplný kód demonstračního příkladu, který tuto činnost provádí, vypadá následovně:

import lxml.etree as ET
 
xml = "pom.xml"
tree = ET.parse(xml)
 
root = tree.getroot()
 
dependencies = tree.xpath("/project/dependencies/dependency")
 
for dependency in dependencies:
    print(dependency.xpath("groupId/text()")[0])

Výsledek:

junit
foo
bar

Výše uvedený postup není příliš rychlý (spíše naopak), ovšem ve skutečnosti můžeme celý problém uspokojivě vyřešit i jiným způsobem, například cestou přímo vracející hodnoty všech poduzlů „groupId“ nalezených na všech cestách odpovídajících „/project/dependencies/dependency/“. Upravený příklad, který by měl být rychlejší, vypadá následovně:

import lxml.etree as ET
 
xml = "pom.xml"
tree = ET.parse(xml)
 
root = tree.getroot()
 
dependency_ids = tree.xpath("/project/dependencies/dependency/groupId/text()")
 
for dependency_id in dependency_ids:
    print(dependency_id)

Výsledek by měl být totožný:

junit
foo
bar
Poznámka: zde ušetříme především několik konstrukcí cesty, protože se interně může jednat o poměrně složitý objekt. U třech závislostí sice nebude rozdíl patrný, ale u velkých XML se stovkami či tisícovkami uzlů již může být rozdíl markantní.

6. Podmínky použité při výběru uzlů pomocí cesty

Při specifikaci cesty je dokonce možné používat i podmínky, v nichž se například testuje hodnota atributů, hodnota uložená v uzlu atd. Před vysvětlením základních podmínek si ukažme příklad, který budeme postupně upravovat. V tomto příkladu se používá cesta, která získá hodnoty všech uzlů „groupId“ (ty se ovšem mohou v dokumentu nacházet na více místech, což jsme již ostatně mohli vidět):

//groupId/text()
Poznámka: zápis dvou lomítek na začátek cesty znamená, že se vyberou všechny uzly „groupId“ nezávisle na tom, ve které části stromu se nachází. Odlišný význam mají dvě lomítka použitá uvnitř cesty.

Získání a výpis hodnoty všech uzlů „groupId“ vypadá takto:

import lxml.etree as ET
 
xml = "pom.xml"
tree = ET.parse(xml)
 
root = tree.getroot()
 
dependency_ids = tree.xpath("//groupId/text()")
 
for dependency_id in dependency_ids:
    print(dependency_id)

Výsledkem budou čtyři řádky, protože se tento uzel nachází jak v popisu vlastního projektu, tak i u všech závislostí (dependency):

org.tisnik.uberproject.test
junit
foo
bar

7. Porovnání hodnoty uzlu s textovým řetězcem

Nyní se nalezené uzly pokusíme omezit pouze na ty, v nichž má „groupId“ hodnotu „junit“. Podmínky se zapisují do hranatých závorek, vlastní text uzlu lze zkrátit na tečku a pro porovnávání se používá jen jediný znak = (na to je zapotřebí si dát pozor, protože dvě == povedou k chybě):

import lxml.etree as ET
 
xml = "pom.xml"
tree = ET.parse(xml)
 
root = tree.getroot()
 
dependency_ids = tree.xpath('//groupId[.="junit"]/text()')
 
for dependency_id in dependency_ids:
    print(dependency_id)

Nyní již získáme pouze jediný výsledek:

junit

Podobně můžeme postupovat i v případě, že budeme chtít omezit množinu uzlů pro testování pouze na informace o závislostech:

import lxml.etree as ET
 
xml = "pom.xml"
tree = ET.parse(xml)
 
root = tree.getroot()
 
dependency_ids = tree.xpath('/project/dependencies/dependency/groupId[.="junit"]/text()')
 
for dependency_id in dependency_ids:
    print(dependency_id)

Jen pro jistotu – dostaneme naprosto stejný výsledek:

junit

8. Složitější cesta s relativní částí

V zápisu cesty je možné použít i dvojici teček (..) pro přesun do jiné části stromu. Tato část je umístěna relativně k uzlu, který byl aplikací cesty nalezen. Vlastně se nejedná o nic složitého, protože podobný styl zápisu používáme i pro přístup k souborům umístěným relativně k pwd. V případě, že budeme například chtít nalézt závislost s groupId nastavenou na „junit“ a následně vypsat verzi takto nalezené knihovny, může cesta vypadat takto:

import lxml.etree as ET
 
xml = "pom.xml"
tree = ET.parse(xml)
 
root = tree.getroot()
 
dependency_ids = tree.xpath('/project/dependencies/dependency/groupId[.="junit"]/../version/text()')
 
for dependency_id in dependency_ids:
    print(dependency_id)

Výsledkem bude verze 3.8.1:

3.8.1

Jen pro doplnění si uveďme, že zápis ./ v cestě znamená „tentýž element“ a tudíž nebude mít vliv na to, jaké elementy budou vybrány popř. jak se bude vyhodnocovat podmínka. Ostatně si to můžete vyzkoušet sami na nepatrně upraveném příkladu se zbytečně komplikovaným zápisem cesty:

import lxml.etree as ET
 
xml = "pom.xml"
tree = ET.parse(xml)
 
root = tree.getroot()
 
dependency_ids = tree.xpath('/project/./././dependencies/dependency/groupId[.="junit"]/.././././version/text()')
 
for dependency_id in dependency_ids:
    print(dependency_id)

9. Knihovna Beautiful Soup

Ve druhé části článku se ve stručnosti seznámíme se základními koncepty, na nichž je postavena knihovna nazvaná Beautiful Soup. Název této knihovny je převzatý ze slavné knihy Alenka v kraji divů (div ovšem nemá nic společného s HTML div-y :-). Tato knihovna dokáže v případě potřeby pracovat s nevalidními soubory XML a samozřejmě i s nevalidními HTML stránkami. Právě v tom ostatně spočívá užitečnost knihovny, protože mnoho HTML stránek (možná většinu?) není možné zpracovávat jako validní XML. Ve skutečnosti dokáže tato knihovna při zpracování HTML a XML používat větší množství parserů, které se od sebe liší rychlostí (či pomalostí) a samozřejmě i svými schopnostmi. Jedná se o tyto parsery:

Parser Stručný popis
„html.parser“ používá parser dodávaný společně s Pythonem
„lxml“ parser HTML převzatý z výše popisované knihovny lxml
„lxml-xml“ parser XML převzatý z výše popisované knihovny lxml
„html5lib“ určený pro parsing nevalidních HTML stránek
Poznámka: kromě prvního parseru je nutné všechny další parsery nainstalovat, a to buď lokálně (pro právě aktivního uživatele) nebo globálně pro všechny uživatele systému.

10. Získání základních informací z HTML stránky

Podívejme se nyní na velmi jednoduchý příklad použití knihovny Beautiful Soup. Pokusíme se v něm získat titulek ze stránky umístěné na adrese „https://www.root.cz“. Vzhledem k tomu, že standardní moduly Pythonu mají problémy s protokolem HTTPS, použijeme pro načtení stránky známou knihovnu requests. Po načtení již můžeme stránku naparsovat, získat první značku title a vypsat její obsah:

import requests
from bs4 import BeautifulSoup
 
response = requests.get("https://www.root.cz")
 
if response.status_code != 200:
    print("Chyba při přístupu na stránku: ", response.status_code)
 
soup = BeautifulSoup(response.text, "html.parser")
 
print(soup.find("title"))
print(soup.find("title").text)

V tomto případě jsme použili standardní parser „html.parser“ a pro získání první značky metodu find. Výsledek:

<title>Root.cz - informace nejen ze světa Linuxu</title>
Root.cz - informace nejen ze světa Linuxu

11. Zjednodušení předchozího příkladu pomocí tečkového operátoru

Předchozí příklad je samozřejmě možné různými způsoby vylepšit, například použitím tečkového operátoru, který zjednodušuje přístup ke značkám a jejich obsahu. Ostatně se o tom můžete přesvědčit sami, zejména po přečtení posledních dvou příkazů v tomto programu:

import requests
from bs4 import BeautifulSoup
 
response = requests.get("https://www.root.cz")
 
if response.status_code != 200:
    print("Chyba při přístupu na stránku: ", response.status_code)
 
soup = BeautifulSoup(response.text,"html.parser");
print(soup.title)
print(soup.title.text)

Výsledek bude stejný, jako tomu bylo v předchozím příkladu:

<title>Root.cz - informace nejen ze světa Linuxu</title>
Root.cz - informace nejen ze světa Linuxu

12. Nalezení všech značek <title> a výpis jejich obsahu

Stránka Roota ve skutečnosti obsahuje větší množství značek <title>. Je tomu tak z toho prostého důvodu, že přímo do stránky je vloženo několik souborů typu SVG. Pokud budeme chtít vyhledat všechny tyto značky, je zapotřebí namísto metody BeautifulSoup.find() použít metodu BeautifulSoup.find_all(). Opět se samozřejmě podíváme na demonstrační příklad:

import requests
from bs4 import BeautifulSoup
 
response = requests.get("https://www.root.cz")
 
if response.status_code != 200:
    print("Chyba při přístupu na stránku: ", response.status_code)
 
soup = BeautifulSoup(response.text, "html.parser")
 
for anchor in soup.find_all("title"):
    print(anchor.text)

S výsledky:

Root.cz - informace nejen ze světa Linuxu
Root.cz
Root.cz
Root.cz
Vitalia.cz
Vitalia.cz
Vitalia.cz
Lupa.cz
Lupa.cz
Podnikatel.cz
Podnikatel.cz
Podnikatel.cz
Vitalia.cz
Root.cz
Mesec.cz
Root.cz
Lupa.cz
Mesec.cz
Vitalia.cz
Root.cz
Podnikatel.cz
Vitalia.cz

13. Výpis všech odkazů ve značkách <a>

Podobným způsobem můžeme zpracovat stránku https://pypi.python.org/simple/ (pozor: je velmi rozsáhlá!) a získat z ní odkazy na všechny balíčky dostupné v repositáři PyPi. Pro zajímavost se pokusíme použít parser „lxml“ pro zpracování HTML stránek dodávaný právě knihovnou lxml:

import requests
from bs4 import BeautifulSoup
 
response = requests.get("https://pypi.python.org/simple/")
 
if response.status_code != 200:
    print("Chyba při přístupu na stránku: ", response.status_code)
 
soup = BeautifulSoup(response.text, "lxml")
 
for a in soup.find_all("a"):
    print(a.text)

Výsledkem by měl být seznam zhruba 15000 abecedně seřazených balíčků:

0
0-._.-._.-._.-._.-._.-._.-0
0.0.1
00SMALINUX
01changer
021
02exercicio
0805nexter
0-core-client
0FELA
0-orchestrator
0wdg9nbmpm
0wned
0x
0x10c-asm
1
100bot
1020-nester
10daysweb
115wangpan
12factor-vault
131228_pytest_1
1337
153957-theme
...
...
...
Poznámka: stránky PyPi prosím nepřetěžujte, ať se v logu neobjeví stovky dotazů z .cz domény.

14. Složitější příklad – získání klíčových slov přiřazených k balíčkům

Další demonstrační příklad, s nímž se dnes seznámíme, je rozdělen na dvě části. První část již známe – slouží k načtení seznamu balíčku z PyPi:

response = requests.get("https://pypi.python.org/simple/")
 
soup = BeautifulSoup(response.text, "lxml")
 
for a in soup.find_all("a")[:10]:
    package_name = a.text
Poznámka: povšimněte si, že počet balíčků ve výsledku omezujeme na 10 prvních nalezených balíčků, a aby nebyla služba PyPi přetížená.

Další část příkladu umístěná v programové smyčce se snaží pro každý balíček získat jeho stránku a tu naparsovat:

    url = urljoin("https://pypi.python.org/pypi/project", package_name)
    response = requests.get(url)
 
    if response.status_code != 200:
        print("Chyba při přístupu na stránku: ", response.status_code)
 
    package_soup = BeautifulSoup(response.text, "lxml")

Ve třetím a současně i posledním kroku se pokusíme přečíst obsah odstavců („p“) s třídou „class“ nastavenou na hodnotu „tags“. Právě v těchto odstavcích jsou umístěna klíčová slova popisující balíček:

    meta_keywords = package_soup.find_all("p", attrs={"class": "tags"})
    if len(meta_keywords) < 1:
        print("Failed to parse and find keywords for '{p}'".format(p=package_name))
        continue
 
    print(meta_keywords)

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

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
 
response = requests.get("https://pypi.python.org/simple/")
 
if response.status_code != 200:
    print("Chyba při přístupu na stránku: ", response.status_code)
 
soup = BeautifulSoup(response.text, "lxml")
 
for a in soup.find_all("a")[:10]:
    package_name = a.text
 
    url = urljoin("https://pypi.python.org/pypi/project", package_name)
    response = requests.get(url)
 
    if response.status_code != 200:
        print("Chyba při přístupu na stránku: ", response.status_code)
 
    package_soup = BeautifulSoup(response.text, "lxml")
    meta_keywords = package_soup.find_all("p", attrs={"class": "tags"})
    if len(meta_keywords) < 1:
        print("Failed to parse and find keywords for '{p}'".format(p=package_name))
        continue
 
    print(meta_keywords)

Z výsledků je patrné, že u některých balíčků nejsou klíčová slova uvedena:

Failed to parse and find keywords for '0'
Failed to parse and find keywords for '0-._.-._.-._.-._.-._.-._.-0'
[<p class="tags">
<i aria-hidden="true" class="fa fa-tags"></i>
<span class="sr-only">Tags:</span>
<span class="package-keyword">
      tensorflow,
    </span>
<span class="package-keyword">
      tfrecord
    </span>
</p>, <p class="tags">
<i aria-hidden="true" class="fa fa-tags"></i>
<span class="sr-only">Tags:</span>
<span class="package-keyword">
      tensorflow,
    </span>
<span class="package-keyword">
      tfrecord
    </span>
</p>]
Failed to parse and find keywords for '00SMALINUX'
Failed to parse and find keywords for '01changer'
Failed to parse and find keywords for '021'
Failed to parse and find keywords for '02exercicio'
Failed to parse and find keywords for '0805nexter'
Failed to parse and find keywords for '0-core-client'
Failed to parse and find keywords for '0FELA'
Failed to parse and find keywords for '0-orchestrator'
Failed to parse and find keywords for '0wdg9nbmpm'
Failed to parse and find keywords for '0wned'
Failed to parse and find keywords for '0x'
[<p class="tags">
<i aria-hidden="true" class="fa fa-tags"></i>
<span class="sr-only">Tags:</span>
<span class="package-keyword">
      notch,
    </span>
<span class="package-keyword">
      asm,
    </span>
<span class="package-keyword">
      dcpu-16,
    </span>
<span class="package-keyword">
      dcpu,
    </span>
<span class="package-keyword">
      assembly,
    </span>
<span class="package-keyword">
      asm
    </span>
</p>, <p class="tags">
<i aria-hidden="true" class="fa fa-tags"></i>
<span class="sr-only">Tags:</span>
<span class="package-keyword">
      notch,
    </span>
<span class="package-keyword">
      asm,
    </span>
<span class="package-keyword">
      dcpu-16,
    </span>
<span class="package-keyword">
      dcpu,
    </span>
<span class="package-keyword">
      assembly,
    </span>
<span class="package-keyword">
      asm
    </span>
</p>]
Failed to parse and find keywords for '1'

15. Další zpracování klíčových slov přiřazených k balíčkům

Ve skutečnosti je možné informace s klíčovými slovy dále zpracovat, protože z předchozího výpisu bylo patrné, že se uvnitř odstavců s třídou „tags“ nachází jednotlivá klíčová slova v samostatných značkách <span> s třídou pojmenovanou „package-keyword“. Těchto značek může být pro jeden balíček několik, takže celé zpracování klíčových slov může vypadat takto:

    print("Keywords for package '{p}'".format(p=package_name))
    keywords_spans = meta_keywords[0].find_all("span", attrs={"class": "package-keyword"})
    for span in keywords_spans:
        for word in span.contents:
            print([k.strip().lower() for k in word.split(",") if k.strip() != ""])
Poznámka: zpracování je v tomto případě nepatrně složitější, protože u některých balíčků nalezneme v jedné značce <span> více klíčových slov oddělených čárkou. Proto se také uvnitř smyčky používá metoda string.split()
.

Opět si ukažme úplný zdrojový kód tohoto příkladu:

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
 
response = requests.get("https://pypi.python.org/simple/")
 
if response.status_code != 200:
    print("Chyba při přístupu na stránku: ", response.status_code)
 
soup = BeautifulSoup(response.text, "lxml")
 
for a in soup.find_all("a")[:20]:
    package_name = a.text
 
    url = urljoin("https://pypi.python.org/pypi/project", package_name)
    response = requests.get(url)
 
    if response.status_code != 200:
        print("Chyba při přístupu na stránku: ", response.status_code)
 
    package_soup = BeautifulSoup(response.text, "lxml")
    meta_keywords = package_soup.find_all("p", attrs={"class": "tags"})
    if len(meta_keywords) < 1:
        print("Failed to parse and find keywords for '{p}'".format(p=package_name))
        continue
 
    print("Keywords for package '{p}'".format(p=package_name))
    keywords_spans = meta_keywords[0].find_all("span", attrs={"class": "package-keyword"})
    for span in keywords_spans:
        for word in span.contents:
            print([k.strip().lower() for k in word.split(",") if k.strip() != ""])

Výsledky by pro několik prvních balíčků měly vypadat takto:

Failed to parse and find keywords for '0'
Failed to parse and find keywords for '0-._.-._.-._.-._.-._.-._.-0'
Keywords for package '0.0.1'
['tensorflow']
['tfrecord']
Failed to parse and find keywords for '00SMALINUX'
Failed to parse and find keywords for '01changer'
Failed to parse and find keywords for '021'
Failed to parse and find keywords for '02exercicio'
Failed to parse and find keywords for '0805nexter'
Failed to parse and find keywords for '0-core-client'
Failed to parse and find keywords for '0FELA'
Failed to parse and find keywords for '0-orchestrator'
Failed to parse and find keywords for '0wdg9nbmpm'
Failed to parse and find keywords for '0wned'
Failed to parse and find keywords for '0x'
Keywords for package '0x10c-asm'
['notch']
['asm']
['dcpu-16']
['dcpu']
['assembly']
['asm']
Failed to parse and find keywords for '1'
Failed to parse and find keywords for '100bot'
Failed to parse and find keywords for '1020-nester'
Keywords for package '10daysweb'
['web']
['framework']
['async']
Keywords for package '115wangpan'
['115']
['wangpan']
['pan']
['cloud']
['lixian']
Failed to parse and find keywords for '12factor-vault'
Failed to parse and find keywords for '131228_pytest_1'
Failed to parse and find keywords for '1337'
Keywords for package '153957-theme'
['photo album']
['theme']
['sigal']
['galleria']

16. Získání tučného textu z HTML stránky

Vraťme se nyní opět ke stránkám Rootu. Další příklad (resp. jeho část) slouží pro zobrazení všech textů umístěných do značky <strong>. Tuto značku často používám pro zvýraznění textu, takže si vyzkoušejme, kolikrát je použita v minulém článku Základy použití režimu org-mode v Emacsu:

response = requests.get("https://www.root.cz/clanky/zaklady-pouziti-rezimu-org-mode-v-emacsu/")
 
if response.status_code != 200:
    print("Chyba při přístupu na stránku: ", response.status_code)
 
soup = BeautifulSoup(response.text, "html.parser")
 
for p in soup.find_all("strong"):
    print(p.text)

Výsledky budou vypadat následovně:

Hlavní navigace
Základy použití režimu org-mode v Emacsu
17 minut
org-mode
vim-orgmode
org-mode
Install
rsync
TAB
TAB
TABu
C-c -
C-c *
M-RETURN
C-c |
C-c -
M-šipka nahoru
M-šipka dolů
C-SPACE
C-c ^
C-c C-c
C-c C-c
C-c C-c
C-c C-x C-b
C-c #
C-c #
TAB
TAB
TAB
TAB
C-c *
C-u C-c *
A1
@2$3
@2$+1
@2$-1
$<
$>
$3
@2
C-c }
C-c }
M-šipka
M-šipka doprava
M-šipka dolů
M-šipka dolů
C-c C-l
vim-orgmode
org-mode
org-mode
Root.cz
Poznámka: drobné nepřesnosti jsou způsobeny tím, že se značka <strong> nachází i v okolním textu mimo hlavní článek.

17. Přečtení obsahu tabulek z HTML stránky

Druhá část příkladu je již mnohem zajímavější, protože se v něm pokusíme zpravovat obsah všech nalezených tabulek. Samotné získání všech tabulek je snadné:

soup = BeautifulSoup(response.text, "html.parser")
 
for table in soup.find_all("table"):
    ...
    ...
    ...

Pro každou tabulku ve vnitřní smyčce získáme všechny řádky představované značkou <tr>:

    for tr in table.find_all("tr"):
        ...
        ...
        ...

A konečně přichází nejzajímavější část celého příkladu – získání všech značek <th> a současně <td>. Povšimněte si, že metodě find_all() můžeme předat seznam značek:

        for item in tr.find_all(["th", "td"]):
            print(item.text + "\t|\t", end="")

Celé zpracování tabulek je záležitost několika programových řádků:

response = requests.get("https://www.root.cz/clanky/zaklady-pouziti-rezimu-org-mode-v-emacsu/")
 
if response.status_code != 200:
    print("Chyba při přístupu na stránku: ", response.status_code)
 
soup = BeautifulSoup(response.text, "html.parser")
 
for table in soup.find_all("table"):
    for tr in table.find_all("tr"):
        for item in tr.find_all(["th", "td"]):
            print(item.text + "\t|\t", end="")
        print()
    print("-------------------")

Výsledek v případě, že jsme se příliš nesnažili o formátování (pouze používáme znak \t):

bitcoin školení listopad 24

Tables
Klávesa |       Význam klávesy (příkazu)        |
TAB     |       změna viditelnosti konkrétního vybraného podstromu (postupně se rotuje mezi různými úrovněmi)   |
S-TAB   |       změna viditelnosti obsahu celého bufferu (dokumentu)    |
C-u C-u TAB     |       výchozí nastavení viditelnosti  |
C-u C-u C-u TAB |       zobrazení obsahu celého souboru, tj. celé jeho struktury        |
-------------------
Klávesa |       Význam  |
M-RET   |       přidání dalšího prvku do (ne)číslovaného seznamu        |
        |               |
C-c –   |       postupná změna typu prvku (číslovaný seznam atd.)       |
C-c ^   |       seřazení seznamu        |
C-c *   |       převedení aktivního prvku na nadpis     |
C-c |   |       převod seznamu (i jiného bloku) na tabulku      |
-------------------
Klávesa |       Význam  |
M-šipka nahoru  |       přesun prvku v rámci seznamu nahoru     |
M-šipka dolů    |       přesun prvku v rámci seznamu dolů       |
-------------------
Objekt  |       Klávesová zkratka       |       Význam klávesové zkratky        |
strom/podstrom  |       TAB     |       změna viditelnosti konkrétního vybraného podstromu (postupně se rotuje mezi různými úrovněmi)   |
strom/podstrom  |       S-TAB   |       změna viditelnosti obsahu celého bufferu (dokumentu)    |
celý dokument   |       C-u C-u TAB     |       výchozí nastavení viditelnosti  |
celý dokument   |       C-u C-u C-u TAB |       zobrazení obsahu celého souboru, tj. celé jeho struktury        |
seznam  |       M-RET   |       přidání dalšího prvku do (ne)číslovaného seznamu        |
seznam  |       C-c –   |       postupná změna typu prvku (číslovaný seznam atd.)       |
seznam  |       C-c ^   |       seřazení seznamu        |
seznam  |       C-c *   |       převedení aktivního prvku na nadpis     |
seznam  |       C-c |   |       převod seznamu (i jiného bloku) na tabulku      |
seznam  |       M-šipka nahoru  |       přesun prvku v rámci seznamu nahoru     |
seznam  |       M-šipka dolů    |       přesun prvku v rámci seznamu dolů       |
checkbox        |       C-c C-c |       zaškrtnutí/zrušení zaškrtnutí políčka   |
checkbox        |       C-c C-x C-b     |       zaškrtnutí/zrušení více políček |
checkboxy       |       C-c #   |       výpočet % nebo zlomku dokončených úkolů |
tabulka |       C-c *   |       přepočet jednoho řádku  |
tabulka |       C-u C-c *       |       přepočet celé tabulky   |
tabulka |       C-c }   |       zobrazení indexů řádků i sloupců        |
tabulka |       M-šipka doleva  |       prohození dvou sloupců  |
tabulka |       M-šipka doprava |       prohození dvou sloupců  |
tabulka |       M-šipka nahoru  |       prohození dvou řádků    |
tabulka |       M-šipka dolů    |       prohození dvou řádků    |
dokument        |       C-c C-l |       vložení odkazu (linku)  |
-------------------

18. Úplný zdrojový kód předchozího demonstračního příkladu

Úplný zdrojový kód demonstračního příkladu, který načte jeden článek z Roota a vypíše všechny tučné texty i obsah všech tabulek, vypadá takto:

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
 
response = requests.get("https://www.root.cz/clanky/zaklady-pouziti-rezimu-org-mode-v-emacsu/")
 
if response.status_code != 200:
    print("Chyba při přístupu na stránku: ", response.status_code)
 
soup = BeautifulSoup(response.text, "html.parser")
 
for p in soup.find_all("strong"):
    print(p.text)
 
print("\n\n\nTables")
 
for table in soup.find_all("table"):
    for tr in table.find_all("tr"):
        for item in tr.find_all(["th", "td"]):
            print(item.text + "\t|\t", end="")
        print()
    print("-------------------")

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 read_pom1.py načtení pom.xml a výpis uzlů pod rootem https://github.com/tisnik/lxml-examples/blob/master/read_pom1.py
2 read_pom2.py výpis informací o projektu: artifact, group a verze https://github.com/tisnik/lxml-examples/blob/master/read_pom2.py
3 read_pom3.py výpis group ID všech knihoven, na nichž projekt závisí https://github.com/tisnik/lxml-examples/blob/master/read_pom3.py
4 read_pom4.py vylepšení předchozího příkladu https://github.com/tisnik/lxml-examples/blob/master/read_pom4.py
5 read_pom5.py použití // v cestě https://github.com/tisnik/lxml-examples/blob/master/read_pom5.py
6 read_pom6.py použití podmínky v cestě https://github.com/tisnik/lxml-examples/blob/master/read_pom6.py
7 read_pom7.py relativní adresování .. v cestě https://github.com/tisnik/lxml-examples/blob/master/read_pom7.py
8 read_pom8.py relativní adresování .. v cestě https://github.com/tisnik/lxml-examples/blob/master/read_pom8.py
9 beautiful_soup1.py získání základních informací z HTML stránky https://github.com/tisnik/lxml-examples/blob/master/beau­tiful_soup1.py
10 beautiful_soup2.py vylepšení předchozího příkladu https://github.com/tisnik/lxml-examples/blob/master/beau­tiful_soup2.py
11 beautiful_soup3.py nalezení všech značek <title> a výpis jejich obsahu https://github.com/tisnik/lxml-examples/blob/master/beau­tiful_soup3.py
12 beautiful_soup4.py výpis všech odkazů ve značkách <a> https://github.com/tisnik/lxml-examples/blob/master/beau­tiful_soup4.py
13 beautiful_soup5.py získání klíčových slov přiřazených k balíčkům https://github.com/tisnik/lxml-examples/blob/master/beau­tiful_soup5.py
14 beautiful_soup6.py zpracování klíčových slov přiřazených k balíčkům https://github.com/tisnik/lxml-examples/blob/master/beau­tiful_soup6.py
14 beautiful_soup7.py získání informací o tabulkách https://github.com/tisnik/lxml-examples/blob/master/beau­tiful_soup7.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. Knihovna 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/
  31. Web scraping
    https://en.wikipedia.org/wi­ki/Web_scraping
  32. Introduction to the POM
    https://maven.apache.org/gu­ides/introduction/introduc­tion-to-the-pom.html
  33. Super POM
    https://maven.apache.org/gu­ides/introduction/introduc­tion-to-the-pom.html#Super_POM
  34. Maven – POM
    https://www.tutorialspoin­t.com/maven/maven_pom.htm
  35. XPath examples
    https://www.w3schools.com/xml/xpat­h_examples.asp
  36. XPath Axes
    https://www.w3schools.com/xml/xpat­h_axes.asp
  37. Guide to naming conventions on groupId, artifactId, and version
    http://maven.apache.org/gu­ides/mini/guide-naming-conventions.html
  38. What is meaning of .// in XPath?
    https://stackoverflow.com/qu­estions/31375091/what-is-meaning-of-in-xpath
  39. Using „//“ And „.//“ Expressions In XPath XML
    https://www.bennadel.com/blog/2142-using-and-expressions-in-xpath-xml-search-directives-in-coldfusion.htm
  40. Using parent dot notation in xpath to find another branch in the XML tree
    https://stackoverflow.com/qu­estions/5370544/using-parent-dot-notation-in-xpath-to-find-another-branch-in-the-xml-tree#5370817

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.