Veebisisu parsimine
Rakendusliideste peatükis vaatasime, kuidas lihtsamaid GET- ja POST-päringuid teha ning JSON-vormingus faile lugeda. Selles peatükis uurime, kuidas Pythoni abil XML- ja HTML-vormingus veebisisu töödelda. Peatüki lõpus tutvustatakse ka RSS-vooge.
HTMLi jaoks on Pythonis sisseehitatud moodul html
, aga seda me siin kasutama ei hakka, sest selles ei ole palju valmisfunktsioone, mida kasutada saaks. XMLi jaoks on vastavalt moodul xml
, millel on märksa rohkem võimalusi. Lisaks kasutame teeke lxml
ning BeautifulSoup
, millega on mugavam antud vorminguid töödelda.
Ettevalmistus
Enne jätkamist tuleb paigaldada teegid BeautifulSoup
ja lxml
käsuga pip install beautifulsoup4 lxml
. Vajadusel loe läbi õpikus moodulite paigaldamise juhised. Peatükk eeldab, et installitud on ka moodul requests
ja läbitud on rakendusliideste peatükk.
HTML- ja XML-failide struktuur
XML-fail koosneb märgendite paaridest, mille vahel võib olla teisi märgendeid või siis tekst. Märgenditel võivad olla ka mingite väärtustega atribuudid. Struktuuri poolest on XML-fail puu, kus igal elemendil (märgendil) võib omakorda olla mingi arv alamelemente.
<sisuga_margend> <nimi>Esimene</nimi> </sisuga_margend> <sisuta_margend/> <silt atribuut_x='4' y>XML-märgendi sisu</silt>
Antud näites on kolm märgendite paari:
- <sisuga_märgend>...</sisuga_märgend>
- <nimi>...</nimi>
- <silt>...</silt>
Kuna <sisuta_märgend/>
ei sisalda midagi, võib ta lühemalt kirja panna ka ilma vastava lõpetava märgendita, aga siis peaks märgendi nime järele olema lisatud kaldkriips.
HTML (HyperText Markup Language) on märgistuskeel, mille süntaks on XMLi (eXtensible Markup Language) süntaksi eriliik, kus on kasutusel samasugune, aga konkreetsem struktuur ning märgendid, mis midagi veebilehel kuvavad. Tüüpilisel HTML5 veebilehel on selline üldkuju:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Veebilehe nimi</title> </head> <body> 'head' märgendis on lehe metainfo, mida veebilehel ei kuvata. 'body' märgendi sees on veebilehe sisu. </body> </html>
Kuna HTML kasutab konkreetseid märgendeid, siis on hea tunda mõningaid tähtsamaid:
Märgend | Lühendi tähendus | Kasutus |
---|---|---|
a | anchor | Lingid |
b või strong | bold | Paks kiri |
i või em | italic või emphasised | Kursiivis kiri |
h1 kuni h6 | heading | Erineva tähtsusega teksti pealkirjad |
p | paragraph | Lõik |
div | division | Üldine blokk elementide grupeerimiseks |
table | - | Tabelite vormistamiseks |
ol | ordered list | Nummerdatud nimekiri |
Kui on tarvis lähemalt HTMLi kohta uurida, võib lugeda õppematerjale eesti keeles, inglise keeles.
Parsimine ja koorimine
Programmi abil faili sisu sõnadeks, fraasideks või märgenditeks jaotamist ja analüüsimist nimetatakse sõelumiseks või parsimiseks (ingl parsing). Veebilehed on olemuselt HTML-failid ja neil on konkreetne struktuur, mille tõttu neid on üsna lihtne parsida. Veebis oleva sisu kogumist ja sõelumist nimetatakse veebikoorimiseks (ingl web scraping).
XMLi parsimine
Oletame, et meil on veebileht või fail, kus andmed on XML-kujul, näiteks Eesti ilmajaamade vaatlusandmed. Andmete kirjelduse leiab siit. Üks võimalus sealt kirjeid välja lugeda oleks kasutada regulaaravaldisi, aga keerukamate failide või andmete puhul võib see olla ebaotstarbekas ning tihti võib ka väga lihtsate andmete kättesaamiseks vaja minna üsna pikki või spetsiifilisi regulaaravaldisi. Sellepärast kasutamegi siinkohal parsimiseks (ehk süntaksianalüüsiks) moodulit xml
:
>>> import requests >>> import xml.etree.ElementTree as ET >>> päring = requests.get("https://www.ilmateenistus.ee/ilma_andmed/xml/observations.php") >>> juur = ET.fromstring(päring.text) >>> esimese_jaama_nimi = juur[0].find("name").text >>> esimese_jaama_nimi 'Kuressaare Linn'
Antud kood pärib ilmaandmete veebilehe sisu ja meetod ET.fromstring
konstrueerib sellest elementide puu, kust on mugav andmeid välja lugeda. Kui elemendil on alamelemente, saab neile ligi nime või indeksi järgi. Näites saadaksegi esimese jaama nimi võttes kõigepealt juurelemendi esimene alamelement ja siis selle name-nimelise elemendi väärtus sõnena.
HTMLi parsimine
HTML-failis elementide otsimiseks ja valimiseks on loodud CSS- ja XPath-avaldised. Neid on ehk mugavam kasutada, sest enamikest veebilehitsejatest saab kopeerida mingi veebilehe elemendi XPath-i või CSS-selektori - vaata näidet Chrome veebilehitsejas. Samas saab käsitsi tihti universaalsema või lühema avaldise kirja panna. Sarnaselt regulaaravaldistele pannakse XPath- ja CSS-avaldisi Pythonis kirja sõnedena.
Kolmas võimalus on oma parsimisreeglitega parseri loomine. Tavaliselt luuakse see Pythoni html.parser.HTMLParser
alamklassina, aga seda me siin käsitlema ei hakka.
XPath-avaldised
Mooduli lxml
abil on võimalik andmeid eraldada sarnaselt mooduliga xml
, kuna esimene on suurelt jaolt viimase peale ülesehitatud. Lisaks lubab see kasutada XPath-avaldisi.
Rakendusliideste lisamaterjali ühe alapealkirja saab näiteks kätte XPath-avaldisega //*[@id="wikitext"]/h3[4]
. Võtame selle osadeks:
//
valib juurelemendi ja kõik selle alamelemendid dokumendipuus.*
ehk metamärk valib kõik seni valitud elemendid (ei täpsusta millise märgendiga elementi valida).[@id="wikitext"]
täpsustab, valib elemendi, millel on atribuut 'id' väärtusega 'wikitext'./h3
valib kõik h3 elemendid, mis on leitud elemendi alamelemendid.[4]
valib leitud h3 elementide seast neljanda (jah, indekseerimine algab 1-st).
All näites tagastab meetod xpath()
loendi kõigist leitud elementidest, aga oleme antud juhul huvitatud ainult esimesest elemendist, valime selle. Vastete puudumisel tagastab meetod tühja loendi.
>>> import requests >>> from lxml import html >>> päring = requests.get("https://courses.cs.ut.ee/2022/programmeerimine/fall/Main/Silmaring2") >>> juur = html.fromstring(päring.content) >>> leitud_elemendid = juur.xpath('//*[@id="wikitext"]/h3[4]') >>> leitud_pealkiri = leitud_elemendid[0].text >>> leitud_pealkiri 'Rakendusliideste pärimine'
Avaldisi saab kokku panna veel näiteks järgmistest osadest:
Avaldis | Tähendus |
---|---|
märgend | Valib kõik antud märgendiga elemendid. |
// | Valib elemendi (vaikimisi juurelemendi) ja kõik selle alamelemendid dokumendipuus. |
/ | Valib kõik mingi elemendi (vaikimisi juurelemendi) otsesed alamelemendid. |
. | Valib hetkel valitud elemendi. |
.. | Valib hetkel valitud elemendi vanema ehk ülemelemendi. |
@atribuut | Valib antud nimega atribuudi. |
[@atribuut] | Valib elemendid, millel on antud nimega atribuut. |
* | Valib kõik elemendid. |
@* | Valib ükskõik millise nimega atribuudi. |
| | Kasutatakse mitme avaldisega valimisel avaldiste eraldamiseks. |
suund:: | Valib elemente mingis suunas (ülemelemendid, eelmised/järgmised naabrid). |
Pikema nimekirja leiab aadressilt: https://nyu-dataservices.github.io/lc-webscraping/xpath-cheatsheet/index.html.
XPath-avaldised on selles osas paindlikumad, et saab valida ülemelemente või naabreid ehk liikuda valimisel mööda dokumendipuud üles või edasi-tagasi. Samuti saab otsida elemente näiteks selle järgi, mis tekst nende nimes sisaldub.
Avaldis | Tähendus |
---|---|
/kuulutus/hind/ancestor::* | Hind-märgendiga elemendi kõik (vana)vanemad. |
//video/parent::* | Kõikide video-märgendiga elementide otsesed vanemad. |
//p/descendant::a | kõikide p-märgendiga elementide kõik a-märgendiga alamelemendid. |
//*[contains(name(), 'i')] | Kõik elemendid, mille märgendis on i-täht. |
//div//p//a[@href=".google."] | Kõikide div-märgendiga elementide kõik p-märgendiga alamelementide kõik a-märgendiga alamelemendid, mille href-atribuut sisaldab sõne .google. . |
//p/text() | Kõikide p-märgendiga elementide tekst. |
CSS-selektorid
CSS-failidega (Cascading Style Sheets) määratakse muuhulgas HTML-elementide stiil ja kujundus. Sealjuures otsitakse ja valitakse dokumendistruktuuris elemente CSS-selektorite (ehk CSS-avaldiste) abil. XPath-avaldistega võrreldes on nende süntaks üldiselt lihtsam, nii et on väiksem tõenäosus avaldistega mööda panna! CSS-selektor toimib kiiremini kui vastav XPath-avaldis, aga otsib dokumendi läbi ainult ühes suunas. See tähendab, et elemendi vanemate või eelnevate elementide valimine on raskem.
CSS-selektori järgi otsimiseks rakendame allolevates näidetes populaarsemat teeki BeautifulSoup
. Soovi korral saab kasutada ka mooduli lxml
meetodit cssselect()
, aga siis tuleks installida lisaks sellenimeline moodul.
>>> from bs4 import BeautifulSoup >>> soup = BeautifulSoup(päring.content, 'html.parser') >>> leitud_elemendid = soup.select("#wikitext > h3:nth-child(23)") >>> leitud_pealkiri = leitud_elemendid[0].text >>> leitud_pealkiri 'Rakendusliideste pärimine'
Selektor lahtivõetuna:
#wikitext
valib kõik elemendid, mille atribuudi 'id' väärtus on 'wikitext'.> h3
valib kõik leitud elementide h3 märgendiga alamelemendid.:nth-child(23)
täpsustab, et tuleks valida element, mis on oma vanema 23. alamelement.
Mõned lihtsamad osad, millest saab selektoreid kokku panna:
Avaldis | Tähendus |
---|---|
märgend | Valib kõik antud märgendiga elemendid. |
selektor1 selektor2 | Valib kõik selektor2-ga valitud elemendid, mis on selektor1-ga valitud elementide alamelemendid. |
selektor1>selektor2 | Valib kõik selektor2-ga valitud elemendid, mis on selektor1-ga valitud elementide otsesed alamelemendid. |
selektor1+selektor2 | Valib selektor2-ga valitud elemendi, mis on selektor1-ga valitud elementide esimene parem naaber. |
selektor1~selektor2 | Valib kõik selektor2-ga valitud elemendid, mis on selektor1-ga valitud elementidest paremal. |
.klassinimi | Valib kõik antud klassinimega (atribuudi 'class' väärtusega) elemendid. |
#id | Valib kõik antud identifikaatoriga (atribuudi 'id' väärtusega) elemendid. |
[atribuut] | Valib kõik antud atribuudiga elemendid (täpsustada võib ka väärtuse). |
: | Kasutatakse teatud täpsustuste järele lisamiseks. |
Pikema nimekirja leiab aadressilt: https://www.w3schools.com/cssref/css_selectors.asp.
Roomajad
Programme, mis automaatselt mitmeid veebilehti külastavad ning koorivad, nimetatakse roomajateks või ämblikeks (ingl vastavalt crawler või spider). Tavaliselt otsivad nad ühelt lehelt üles kõik lingid ja liiguvad nende kaudu edasi. Otsingumootorid täiendavad ja uuendavad nii oma otsingutulemusi. Samuti saab niimoodi sotsiaalmeediat koorida ning paljud firmad hoiavad sel viisil silma peal oma reputatsioonil. See on kasulik ka siis, kui on vaja pidevalt olla kursis uute konkureerivate toodete ja hindadega: ämblik võib regulaarselt internetivõrgus ringi sibada ja hindade kohta andmeid koguda.
Roomajate abil võib ka veebilehtedele sisse logida. Mõned veebilehed lubavad teatud sisu koorida ainult registreeritud kasutajatel, kes on taotlenud selle jaoks võtme.
Kui veebilehed mingil põhjusel üldse ei soovi, et neid kasutaksid mingid roomajad või koorijad, luuakse takistusi. Näiteks enne lehe sisu kuvamist lisatakse mehhanism, mis kontrollib, kas lehte loeb inimene või robot. See võib paluda vastata mingile küsimusele, mis on inimese jaoks lihtne, aga arvutile raske.
Näide
Ütleme, et on tarvis veebis roomavat programmi, mis leiaks esimeselt lehelt kõik lingid, mis on vormistatud suuremate pealkirjadena ja uuriks, mis on iga lingi aadressil oleva lehe sisu. Lehe struktuuris võib lingiga pealkirja vormistada kahel viisil:
- Link pealkirja sees:
<h2><a href="aadress">Lingi tekst</a></h2>
- Pealkiri lingi sees:
<a href="aadress"><h2>Lingi tekst</h2></a>
CSS-selektoriga h2 a
saaks valida esimese. Kui veab, siis töötab teise kuju leidmiseks ka uuem süntaks a:has(h2)
, aga ametlikult ei saa CSS-ga veel valida elemente lähtudes alamelementidest. Sel moel vormistatud linkide valimiseks võib kõigepealt selektoriga leida märgendid nimega h2
ja uurida programmis igaühe puhul, mis ta vanem on:
for elem in soup.select("h2"): if elem.parent.name == "a": ... # Otsene vanem on <a>
XPath-avaldisega saab mõlemad kujud valida. Selleks sobib nt. avaldis //h2//a | //h2/ancestor::a
. Ütleme, et on fail, kus võib leiduda h2- ja h3-märgendiga pealkirju ja tahame nüüd leida sellised, mis on ka lingid:
from lxml import html faili_sisu = """ <a href="aadress1"> <h2>Pealkiri 1</h2> </a> <div> <h3> <a href="aadress2">Alampealkiri 1</a> </h3> <h3>Alampealkiri 2</h3> <a href="aadress3">Suvaline link</a> </div> <h2> <i>Pealkiri 3: <a href="aadress4">link</a></i> </h2> """ juur = html.fromstring(faili_sisu) aadressid = juur.xpath("//*[self::h2 or self::h3]//a/@href | //*[self::h2 or self::h3]/ancestor::a/@href")
Reegli *[self::märgend1 or self::märgend2]
abil valitakse mitu võimalikku märgendit ja /@href
valib aadressi väärtuse. Tulemus:
>>> aadressid ['aadress1', 'aadress2', 'aadress4']
RSS-vood
Paljudel veebilehtedel on saadaval XML-kujul uudisvoog ehk RSS-voog, mis ongi mõeldud pigem koorimiseks kui veebilehitsejaga lugemiseks. See koosneb kirjetest, mis viitavad mingitele resurssidele, tavaliselt uutele postitustele. Lisaks nimele või pealkirjale on ühes kirjes tavaliselt resurssi aadress, kirjeldus, illustreeriv pilt vms. RSS-lugerid on programmid, mis loevad mingist uudisvoost välja värskeid uudisepealkirju, postitusi, videoid vms.
Veebilehe uudisvoo aadressi leiab tavaliselt veebilehel vastavale sümbolile vajutades või otsides lähtekoodist võtmesõna 'rss', 'atom' või 'feed'. Allpool on toodud otselingid mõningate veebilehtede RSS-voogudele.
- Tartu RSS-voog (tartu.ee lehelt)
- Tartu ilm (Accuweather)
- Haridusministeeriumi uudisvoog
- Rahva oma kaitse saated (Raadio 2 lehelt)
- Estonian World Review (teatud uudisteportaal)
- Igapäevased anekdoodid (jokesoftheday.net)
- Programmeerimishuumori voog (devhumor.com)
- Eesti vandenõuteooriate foorumi postitused (Para-web foorum)
- Eesti talunike müügi- ja ostukuulutused (elavtoit.com)
- Kasutajate postitatud mõistatused (Puzzling Stack Exchange)
RSS-vood on muuhulgas ka Youtube'i kanalitel ja enamikel taskuhäälingutel (podcastidel).
RSS-voo lugemine
RSS-voogudel on veel konkreetsem struktuur kui tavalistel XML- või HTML-lehtedel. Näiteks talutoidu ostu-müügi kuulutuste voos on iga kirje kohta ainult kuulutuse pealkiri, link, kirjeldus ja avaldamise aeg:
... <item> <title>ELAVTOIT MÜÜK: Tom</title> <link>http://www.elavtoit.com/pakkumine/talu/id_1375</link> <description> Mahe, Muna, Maheda toiduga söödetud ja vabaeluga kana munad, 10, 1, 3, kalameestom@online.ee, 56811236, , , 0, 0, 0, 0 </description> <pubDate>2022-04-14 12:45:02</pubDate> </item> ...
Siin piisab, kui valime CSS-selektori abil kõik elemendid märgendiga item
ja valime seejärel iga elemendi alamelemente eraldi. Siis saaks voo niimoodi kuvada:
import requests from bs4 import BeautifulSoup päring = requests.get("http://www.elavtoit.com/rss_elavtoit.php") soup = BeautifulSoup(päring.content, 'html.parser') kirjed = soup.select("item") # Väljastab esimesed kolm: for kirje in kirjed[:3]: pealkiri = kirje.title.text.strip() kirjeldus = kirje.description.text.strip() print("KUULUTUS:", pealkiri) print("KIRJELDUS:", kirjeldus) print()
>>> %Run kuulutused.py KUULUTUS: ELAVTOIT MÜÜK: Tauri Plaado KIRJELDUS: , Pardimunad, Müüa pekingi pardimune, 10, 5, 0,5, tauri.plaado@gmail.com, 58361238, , , , Kingu Kanepi vald , 58.0558, 26.6643 KUULUTUS: ELAVTOIT MÜÜK: Tom KIRJELDUS: Mahe, Muna, Maheda toiduga söödetud ja vabaeluga kana munad, 10, 1, 3, kalameestom@online.ee, 56811236, , , 0, 0, 0, 0 KUULUTUS: ELAVTOIT MÜÜK: Tauri Plaado KIRJELDUS: , Rabarberi täismahl 1,5L mahlakott, Pakendatud sangaga mahlakotti. Külmpressitud ja pastöriseeritud. , 1,5, 1,5 L, 6,50, tauri.plaado@gmail.com, 58361238, , , , Kingu Kanepi vald , 58.0558, 26.6643
Kirjelduste lõpus on koma ja tühikuga eraldatud veel müüdava kauba ja müüja andmed. Kui ei soovi komadega teksti tükeldamisega tegeleda, on vastaval veebilehel andmed ka tabelkujul, mida on tegelikult peaaegu sama lihtne parsida: http://www.elavtoit.com/tabel/pakkumine. Mõne veebilehe RSS-voog võib sisaldada rohkem eraldatud andmeid, nt. üks kirje Puzzling Stack Exchange lehelt näeb välja selline:
<entry> <id>https://puzzling.stackexchange.com/q/116105</id> <re:rank scheme="https://puzzling.stackexchange.com">1</re:rank> <title type="text">A donut, a piece of string and a pair of spectacles</title> <category scheme="https://puzzling.stackexchange.com/tags" term="geometry" /> <author> <name>loopy walt</name> <uri>https://puzzling.stackexchange.com/users/73836</uri> </author> <link rel="alternate" href="https://puzzling.stackexchange.com/questions/116105/a-donut-a-piece-of-string-and-a-pair-of-spectacles" /> <published>2022-05-12T04:05:47Z</published> <updated>2022-05-12T04:05:47Z</updated> <summary type="html"> <p>This is a simplified version of ... ... </summary> </entry>
Enesekontrolliküsimused
Ülesanded
1. Kirjuta ilmavaatlusprogramm, mis tagastab valitud ilmajaamas viimati mõõdetud andmed.
Näide:
>>> %Run ilmavaatlus.py Sisesta ilmajaama nimi: Jõgeva Jõgeva jaamas mõõdetud õhutemperatuur on -0.3 kraadi, õhurõhk on 983.6, nähtavus on 0.8, tuulekiirus on 3.0 - 9.5 m/s. Nähtus: Nõrk lumesadu.
2. Leia mõni enda jaoks huvitav RSS-voog ja kirjuta programm, mis loeb seda ning väljastab loendi selle kirjetest. Ühe kirje kohta võib kuvada selle nime, lingi ja kuupäeva või kirjelduse. Lisaülesanne: Kasuta moodulit webbrowser
, et avada kasutaja valikul mingi kirje link.