Današnje predavanje bo imelo dva do tri smotre.
Markdown je preprost jezik, namenjen osnovnemu oblikovanju besedil: omogoča naštevanje, krepak in poševen tisk, citiranje, vstavljanje naslovov programske kode in slik ter spletne povezave. Uporabljamo ga povsod na spletu (in še kje drugod), kjer bi želeli nekoliko oblikovano besedilo. V markdownu so napisani tile zapiski, vse, kar objavljam na Učilnici, z njim lahko pišete komentarje na različne spletne strani.
Jezika se je trivialno naučiti: kar zapremo med zvezdice, je izpisano
poševno (ker sem napisal *poševno*
), če sta
zvezdici dve, pa krepko (ker sem napisal
**krepko**
). Alineje zapišemo z minusi in se izpišejo
če jih želimo oštevilčiti,
Pred naslove damo en, dva, tri, štiri ali kolikor je treba znakov
#
(več ko jih je, manj pomemben je naslov).
Povezave zapišemo
[tako, da v oglate oklepaje damo besedilo](https://ucilnica.fri.uni-lj.si)
,
v oklepajih pa povemo, kam povezava vodi.
Vse, kar damo v vzvratne narekovaje (backtick), se izpiše kot "koda"; zgoraj sem to uporabljal, da se je besedilo med zvezdicami ali v povezavi izpisalo z zvezdicami in oglatimi oklepaji, ne pa spremenilo v poševen in krepak tisk ali v povezavo.
Kdor hoče izvedeti več bo pogledal na plonkec, vendar tudi tam ni kaj več, saj dosti več ne obstaja.
Besedilo znotraj teh zapiskov je napisano v markdownu - znotraj Jupytrovega notebooka. Uporabljamo ga že vse leto, z neveliko uvoda na začetku. Po eni strani ga ni težko začeti uporabljati, po drugi pa zna biti frustrirajoč, kadar želiš kaj narediti drugače, kot hoče.
Za tako pomemben in vseprisoten projekt ima fascinantno zanič dokumentacijo za nekoga, ki ni ravno računalnikar. Na uradnih straneh je še najbolj uporabno poglavje Network basics. Vse ostalo, kar sem našel, so bile strani na raznih "content farmih" in spletnih mestih, ki niso dovolj odprta, da bi nanje pošiljal študente.
Toliko o tem v zapiskih. Kdor hoče vedeti več, naj vpraša na predavanjih.
Spletne strani so zapisane v jeziku HTML. Ta je podoben XML, ki smo ga spoznali prejšnjič.
XML je splošnejši. XML služi zapisu in prenašanju poljubnih podatkov
- od zapisov pretečenih ali prekolesarjenih poti v Stravi s
standardiziranim formatom GPX do vektorskih slik v formatu SVG. Vsak od
njih uporablja svoj specifičen nabor značk, ki jih zapisujemo v
<...>
. Vsak element je potrebno začeti in zaključiti
(na primer <track>
za začetek opisa poti in
</track>
za konec) in biti morajo pravilno gnezdeni.
Če je znotraj elementa <track>
element
<trkpoint>
, je potrebno slednjega zapreti pred
prvim.
Tako kot sta GPX in SVG konkretni obliki XMLja, namenjeni zapisovanju poti in vektorskih slik, je tudi HTML oblika XMLja, namenjena opisovanju spletnih strani. Bistvena - in za na žal pomembna - razlika je v tem, da so bralniki za HTML bolj tolerantni glede zapiranja elementov. Če, na primer, nek element zapremo prezgodaj, preden smo zaprli notranje, bo bralnik kar sam od sebe zaprl tudi notranje. Če zapremo element, ki ga sploh nismo odprli, bo bralnik to kar ignoriral. XML mora imeti en sam korenski element, ki vsebuje vse ostale. HTML ga ima samo, če je lepo vzgojen. Če ni, bralnik pač skomigne z rameni in dokument vseeno normalno prebere. Tako so naredili zato, ker so bile v davnini spleta spletne strani pogosto natipkane ročno, kot HTML in tipkali so jih tudi ljudje, ki niti niso računalnikarji, zato smo se jih (računalnikarji) usmilili. Spletno stran je bilo vseeno možno pokazati in če je bila zaradi napak v uporabi značk videti čudno, jo je avtor pač popravil.
Posledično pa HTMLja ni tako preprosto brati s programom. Pravzaprav sta razloga dva.
Da bomo lahko iz HTML karkoli prebrati, moramo vsaj približno razumeti njegovo strukturo.
Tule so osnovne značke (osnovne v smislu, da jih bomo potrebovali pri razbiranju podatkov, ne sestavljanju strani).
<p>
je odstavek, paragraph. HTML ignorira prazen
prostor, vključno s praznimi vrsticami, zato je potrebno odstavke
označiti s P.<div>
in <span>
sta enoti
znotraj besedila. Kaj pomenita in kakšna je razlika med njima, je za nas
nepomembno, omenjamo ju zato, ker bo tisto, kar iščemo, pogosto znotraj
DIV-ov z določeno oznako.<h1>
, <h2>
,
<h3
> ... so naslov, podnaslov, podpodnaslov in tako
naprej.<a>
je hiperpovezava. Ta nas bo najbrž najbolj
zanimala.<img>
je slika.<ol>
in <ul>
sta oštevilčeni
(ordered) in neoštevilčeni (unordered) seznam. Znotraj njiju bodo
alineje, zapret v <li>
.Elementi imajo atribute, ti bodo pri iskanju podatkov pomembni, ker ima stran lahko ogromno značk DIV ali A, nas pa zanimajo tiste, ki so označene z določenim atributom. Tule so najpomembnejši
class
določa oblikovanje elementov. Kako to počne, ni
pomembno. Pomembno je tole: če imamo spletno stran z naslovi filmov,
bodo vsi naslovi verjetno oblikovani enako - enaka pisava, oblika in
tako naprej. Naslovi bodo morda v elementih <div>
in
vsi bodo imeli določen, na primer
<div class="movie-title" ...>
ali
<div class="title-of-the-movie" ...>
ali
<div class="ipc-metadata-list-summary-item__t">
...
dokler ne pogledamo, ne moremo vedeti. Če hočemo priti do naslovov
filmov, torej poberemo vse <div>
-e, imajo določen
želeni razred. Slike s posterji filmov, ki jih vidimo zraven povezav, so
mogoče img
z razredom ipc-image
(ampak samo na
IMDB). Razredi so kot nekakšne spremenljivke, ki si jih sestavljalci
strani izmislijo sami, mi pa jih lahko pogledamo.id
je malo podoben, vendar popolnoma drugačen. :) Id-ji
so enkratni: na vsaki strani lahko obstaja le en element s takšnim
id-jem. Id-ji lahko določajo tudi obliko, vendar pogosto služijo tudi
temu, da program, ki teče znotraj strani, prebira ali spreminja id-je. Z
id-je je lahko označen kak naslov strani (ki morda vsebuje podatek, ki
ga želimo prebrati). Primer - če smo ravno na IMDB, je
imdbHeader
, s katerim je označena vrstica na vrhu
strani.href
je atribut značke a
(in še nekaterih,
ki nas ne zanimajo). Atribut href
vsebuje URL, na katerega
kaže povezava. To nam bo očitno prišlo prav, če bomo hoteli napisati
program, ki prebere vse povezave na določene podstrani in prebere, na
kar kažejo te povezave.name
prav tako najdemo ob znački a
. Ta ne
pomeni povezave na spletno stran, temveč označi mesto znotraj te spletne
strani, tako da se lahko druge strani sklicujejo nanj. Več o tem
spodaj.src
je atribut elementa img
. Vsebuje
povezavo do slike. Če v Pythonu z urlopen
odpremo (in potem
z read
preberemo) url, zapisan v href
bomo
dobili bajte (upam, da se še spomnimo - to je tisto, kar je podobno
nizu, vendar ni sestavljeno iz znakov temveč iz številk), ki jih lahko,
recimo, shranimo v datoteko z ustrezno končnico, pa imamo sliko.Tako atributov kot značk je še na stotine in stotine. Tule smo jih spoznali toliko, da bo dovolj za nekaj osnovnih primerov in vaj.
Povedati moramo še nekaj o URLjih. Ti so, podobno kot poti do datotek, lahko absolutni ali relativni in še hujše.
http://
ali https://
,
vodi na povsem drug strežnik. (Ali na istega, le s predolgim URL-jem.)
Sledil bo seveda naslov strežnika, na primer
ucilnica.fri.uni-lj.si
.ftp://
ali mailto://
ali
kajdrugega://
, potem ne vodi na spletno stran temveč na
kakšen drugačen strežnik ali kam čisto drugam. Povezava
mailto://
, recimo, povzroči odpiranje programa za
pošiljanje pošte./
, gre, podobno kot
pri datotekah za absolutno pot, seveda znotraj obstoječega strežnika. Če
na strani http://primer.com/primer/uporaba.html
naletimo na
URL /nekaj/drugega.html
, bo vodil na stran
http://primer.com/nekaj/drugega.html
. Strežnik, torej,
ostane isti, pot pa se zamenja z novo./
(ali celo
http://
), predstavlja relativno pot. Če na strani
http://primer.com/primer/uporaba.html
naletimo na URL
/nekaj/drugega.html
, ta najbrž vodi na
http://primer.com/primer/nekaj/drugega.html
. Povezava je
torej najbrž relativna glede na pot, na kateri je trenutna
stran. Zakaj najbrž? Zato ker lahko spletna stran določi
drugače in sicer tako, da znotraj elementa body
doda
element <base href="neka-druga-pot" />
.http://primer.com/primer/uporaba.html
vsebuje
<a name="pomembnomesto">
, bo povezava
http://primer.com/primer/uporaba.html#pomembnomesto
vodila
na to stran in to na to konkretno mesto - brskalnik bo sam poskrolal
stran do tja. Del za #
lahko dodamo k absolutnim ali
relativnim potem, ali celo brez njih. Če znotraj strani postavimo URL
#pomembnomesto
, klik na ta URL ne bo vodil na drugo stran,
temveč bo brskalnik le oddrsel na to mesto.Kot za vse v teh zapiskih, velja tudi za URL: spletni razvijalci niso brez razloga med najboljše plačanimi programerji. Tule zgolj približno (in mestoma narobe) praskamo po površju nečesa, kar zahteva leta treninga.
Za branje HTML bomo uporabili knjižnico Beautiful Soup. Te ne dobimo s Pythonom, temveč si jo moramo namestiti dodatno.
V Jupytru to najlažje storimo tako, da vtipkamo
%pip install bs4
in po potrebi ponovno poženemo Python (Kernel / Restart).
Zdaj bomo s strani projekta Gutenberg prebrali imena vseh avtorjev,
katerih priimek se začne s črko B. Najdemo jih na strani
https://www.gutenberg.org/browse/authors/b
.
from urllib.request import urlopen
= urlopen("https://www.gutenberg.org/browse/authors/b").read() html
Kot se, upam, spomnimo, smo dobili bajte - poglejmo začetek.
100] html[:
b'<!DOCTYPE html>\n<html class="client-nojs" lang="en" dir="ltr">\n<head>\n <meta charset="UTF-8"/>\n\n<tit'
Videti je kar normalno, kot ASCII, vendar kasneje vsebuje vse mogoče, od portugalski obvijuganih a-jev, pa do kitajščine, zato ga moramo odkodirati kot UTF. (Mimogrede, tudi sam, že v zgoraj napisanem, pove, da je njegov "charset" enak "UTF-8".
= html.decode("utf-8") html
100] html[:
'<!DOCTYPE html>\n<html class="client-nojs" lang="en" dir="ltr">\n<head>\n <meta charset="UTF-8"/>\n\n<tit'
In zdaj preberemo HTML v drevo značk, hvala Beautiful Soup.
from bs4 import BeautifulSoup
= BeautifulSoup(html) soup
Pa zdaj? Zdaj je potrebno zares pogledati spletno stran, njeno kodo in ugotoviti, v kakšnih značkah so zapisana imena avtorjev.
Gremo na stran in pritisnemo Ctrl-Alt-I na Windows oz. Cmd-Alt-I na macOS; to deluje v Chromu, Firefoxu in Safariju, kdor uporablja kaj, česar nisem poskusil, pa naj poskuša sam. V spodnjem (ali desnem, odvisno od nastavitev) delu okna se pojavi drevo s strukturo HTML, ki ga gledamo v gornjem (ali levem) delu.
Če se zdaj z miško vozimo po elementih drevesa v spodnjem delu, nam osvetljuje pripadajoče elemente v gornjem. In obratno: če izberemo tisti kvadratek s puščico, ki je v vrstici med zgornjim in spodnjim delom okna, lahko klikamo na elemente v gornjem delu in v spodnjem se pokaže ustrezni element drevesa. Če, recimo, kliknem Charlesa Babbagea, se zgodi tole.
Očitno so imena avtorjev znotraj H2, torej kot podnaslovi. Vsebujejo
dve znački a
. Prva nastavi povezavo znotraj strani,
name
, na neko kodo - za Babbagea je to a556. To pomeni, da
nas povezava na stran
https://www.gutenberg.org/browse/authors/b#a556
(URL te
strani z dodatnim #556
) namesto na začetek strani postavi
na Babbagea. Znotraj te povezave je zapisano njegovo ime in letnici
rojstva in smrti. Sledi še en element a
, ki vsebuje
besedilo "¶". povezava pa kaže na https://www.gutenberg.org/browse/authors/b#a556. Če
kliknemo ta ¶, se stran ne spremeni, spremeni pa se naslov v brskalniku,
tako da ga lahko skopiramo ali shranimo. (Zaradi prvega a
je tudi ime videti kot povezava, vendar ne naredi ničesar. Take imam res
rad.)
Zdaj najbrž vemo, kje poiskati imena avtorjev: poskusili bomo kar vse
elemente a
, ki vsebujejo name
. Zgodi se lahko
troje
Seveda je v igri tudi varianta 4, po kateri sicer ne dobimo vseh imen avtorjev, pač pa dobimo dodatno šaro.
Če se bo zgodilo prvo, bomo razmislili, kako bi bili bolj specifični.
Morda bomo rekli, da mora biti ta a
znotraj
h2
. Če se bo zgodilo drugo, bomo morali pregledati
manjkajoče avtorje in razmisliti, kaj je specifičnega zanje, kako jih
najti in ali bi lahko na enak način našli tudi druge.
Poslušajmo latince, ki so vedeli povedati, da audentes fortuna
iuvat. soup
ima metodo find_all
, ki kot
argument prejme ime elementa in vrne vse elemente s tem imenom.
"a")[:10] soup.find_all(
[<a class="no-hover" href="/" id="main_logo">
<img alt="Project Gutenberg" draggable="false" src="/gutenberg/pg-logo-129x80.png"/>
</a>,
<a href="/about/">About
<span class="drop-icon">▾</span>
</a>,
<a href="/about/">About Project Gutenberg</a>,
<a href="/policy/collection_development.html">Collection Development</a>,
<a href="/about/contact_information.html">Contact Us</a>,
<a href="/about/background/">History & Philosophy</a>,
<a href="/policy/permission.html">Permissions & License</a>,
<a href="/policy/privacy_policy.html">Privacy Policy</a>,
<a href="/policy/terms_of_use.html">Terms of Use</a>,
<a href="/ebooks/">Search and Browse
<span class="drop-icon">▾</span>
</a>]
Kar takoj posvarimo: župa se obnaša nenavadno. Če se zatipkamo pri
imenu metode, vrne None
namesto
AttributeError
.
print(soup.tralala)
None
To počne zato, ker nam soup.tralala
ali
soup.kajdrugega
vrne (prvo) značko tralala
oziroma kajdrugega
v dokumentu. Če ne obstaja (točneje: ker
ne obstaja), pač vrne None
namesto napake. To nas tepe, če,
recimo, pozabimo _
v find_all
.
"a") soup.findall(
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[8], line 1
----> 1 soup.findall("a")
TypeError: 'NoneType' object is not callable
soup.findall
je None
, zato ga ni mogoče
poklicati niti z "a"
niti s kakimi drugimi argumenti.
Oziroma sploh.
No, če smo se že naučili, kako poceni priti do elementa
a
, pograbimo kar prvega, ki pride pod roko.
= soup.a link
Kako izvemo, da se v njem ne skriva ime avtorja? Kot smo rekli:
elementi z imeni avtorjev. Vse elementi župe imajo atribute
attrs
, ki vsebuje vse atribute. Del HTMLja, ki vsebuje naš
link
, je
link
<a class="no-hover" href="/" id="main_logo">
<img alt="Project Gutenberg" draggable="false" src="/gutenberg/pg-logo-129x80.png"/>
</a>
Atributi so class
, href
in id
.
Res je:
link.attrs
{'id': 'main_logo', 'href': '/', 'class': ['no-hover']}
(Atribut class
je seznam, ker lahko element pripada več
razredom. Mimogrede tu vidimo še en primer uporabe id-ja: označuje
main_logo. Najbrž zgolj zaradi oblikovanja: v pravilih za oblikovanje
strani so določili, kako naj bo oblikovan element z id-jem
main_logo
, namesto elementi v razredu
main_logo
; tako so storili zaradi higiene, saj stvari, ki
so edinstvene, niso "razred zase".)
Zdaj znamo poiskati elemente z avtorji: v slovarju attrs
morajo imeti name
. Poiščimo prvega, da se bomo lahko igrali
z njim in ugotovili, kako izvleči ime avtorja.
for link in soup.find_all("a"):
if "name" in link.attrs:
break
link
<a name="a32541">Baader, Bernhard, 1790-1859</a>
Da, vidite je, da smo ujeli človeka. Najbrž res prvega po abecedi med
vsemi na B
. :)
Elementi imajo atributa string
in
strings
.
link.string
'Baader, Bernhard, 1790-1859'
link.strings
<generator object Tag._all_strings at 0x10d7048b0>
Drugi je videti manj privlačno. :)
Pripravimo slovar, katerega ključi bodo avtorji, vrednosti njihove notranje povezave na strani.
= {}
authors for link in soup.find_all("a"):
if "name" in link.attrs:
= link.attrs["name"]
authors[link.string]
len(authors)
3545
list(authors)[:10]
['Baader, Bernhard, 1790-1859',
'Baadsgaard, Anna, 1865-1954',
'Baarslag, C.',
'Bååth, A. U. (Albert Ulrik), 1853-1912',
'Babbage, Charles, 1791-1871',
'Babbitt, Ellen C.',
'Babbitt, George Franklin, 1848-1926',
'Babbitt, Harold E. (Harold Eaton), 1888-1970',
'Babbitt, Irving, 1865-1933',
'Babbitt, Katharine']
Odlično: dobili smo samo avtorje in, če na hitro pogledamo, so najbrž vsi.
Ker bi bilo s tem slovarjem zdaj lepo nekaj narediti, ga shranimo v Markdown. :) Ta bo vseboval same alineje: vsaka bo ime avtorja in bo delovala kot povezava na stran avtorjev na B, konkretno na točno tega človeka znotraj strani.
= open("avtorji-na-b.md", "w")
f = "https://www.gutenberg.org/browse/authors/b"
url for auth, aname in authors.items():
f"- [{auth}]({url}#{aname})\n")
f.write( f.close()
Kam s to datoteko? Uporabimo jo pač kje, kjer sprejemajo Markdown. Lahko, recimo, skopiramo nekaj prvih vrstic sem, v celico notebooka.
In potem kliknemo povezavo, da vidimo, ali res deluje.
Koda, s katero smo iskali elemente a
, je bila brez
potrebe prezapletena. find_all
sprejema še več argumentov;
med drugim lahko zahtevamo, da ima element določen atribut.
Takole dobimo vse a
-je, ki imajo definiran
id
. Izpišimo prvih deset:
"a", id=True)[:10] soup.find_all(
[<a class="no-hover" href="/" id="main_logo">
<img alt="Project Gutenberg" draggable="false" src="/gutenberg/pg-logo-129x80.png"/>
</a>]
Takole pa bi dobili a
, katerega id
je enak
main_logo
.
"a", id="main_logo") soup.find_all(
[<a class="no-hover" href="/" id="main_logo">
<img alt="Project Gutenberg" draggable="false" src="/gutenberg/pg-logo-129x80.png"/>
</a>]
V resnici nas ne briga id
. Najti želimo tiste, ki imajo
določen name
.
"a", name=True) soup.find_all(
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[21], line 1
----> 1 soup.find_all("a", name=True)
TypeError: Tag.find_all() got multiple values for argument 'name'
Zato sem kot primer uporabil ne prav smiselni id
.
Problem je v tem, da je name
ravno ime prvega argumenta,
tistega, ki smo mu dali vrednost a
. Python se zato razjezi,
da vrednosti argumenta name
ne smemo nastaviti dvakrat,
enkrat na "a"
in potem še na True
.
Podobno je s class
.
"a", class="no-hover") soup.find_all(
Cell In[22], line 1
soup.find_all("a", class="no-hover")
^
SyntaxError: invalid syntax
Beseda class
je rezervirana in ne more biti ime
spremenljivke ali argumenta. Kadar želimo povpraševati po
name
ali class
, jih moramo podati s
slovarjem.
"a", {"name": True})[:10] soup.find_all(
[<a name="a32541">Baader, Bernhard, 1790-1859</a>,
<a name="a52090">Baadsgaard, Anna, 1865-1954</a>,
<a name="a26759">Baarslag, C.</a>,
<a name="a47041">Bååth, A. U. (Albert Ulrik), 1853-1912</a>,
<a name="a556">Babbage, Charles, 1791-1871</a>,
<a name="a2498">Babbitt, Ellen C.</a>,
<a name="a56365">Babbitt, George Franklin, 1848-1926</a>,
<a name="a51605">Babbitt, Harold E. (Harold Eaton), 1888-1970</a>,
<a name="a45766">Babbitt, Irving, 1865-1933</a>,
<a name="a42830">Babbitt, Katharine</a>]
Pogojnega stavka pri sestavljanju slovarja se lahko znebimo.
= {}
authors for link in soup.find_all("a", {"name": True}):
= link.attrs["name"] authors[link.string]
Ali pa napišemo kar
= {link.string: link.attrs["name"] for link in soup.find_all("a", {"name": True})} authors
Pridobivanje podatkov s spletnih strani je lahko enostavno, če znamo in če so prijazno sestavljene...
Zdaj si zadajmo težjo nalogo. Dobimo avtorja ali, da bo preprosteje, kar njegovo interno oznako - za Babbaga, recimo a556. Naloga je poiskati vsa njegova dela, objavljena na Gutenbergu.
Če jih vidimo na spletni strani, so očitno v dokumentu, le do njih
moramo znati priti. Vrnemo se v brskalnik, v spodnjem delu odpremo
elemente pod h2
.
Vidimo strukturo? Začnemo pri a
. Njegov oče je
h2
. Brat h2
-ja je ul
.
ul
-jevi otroci vsebujejo posamična dela.
Najprej torej zgrabimo Babbaga. Ker vemo, da je na strani le en
a
, katerega name
je enak a556
,
namesto find_all
(čez katerega bi morali z zanko),
pokličemo kar find
, ki vrne prvi - in v tem primeru edini -
element, ki ustreza podanim argumentom.
= soup.find("a", {"name": "a556"})
link
link
<a name="a556">Babbage, Charles, 1791-1871</a>
Njegov oče je shranjen v parent
.
link.parent
<h2><a name="a556">Babbage, Charles, 1791-1871</a> <a href="#a556" title="Link to this author">¶</a></h2>
Njegov brat je next_sibling
.
link.parent.next_sibling
'\n'
Eh. Ker je v HTMLju tu vmes znak za prazno vrstico (isto smo videvali tudi v XML, nismo?), je brat ta prazna vrstica. Najbrž bi bilo potrebno vzeti naslednjega brata?
link.parent.next_sibling.next_sibling
<ul>
<li class="pgdbxlink"><a href="https://en.wikipedia.org/wiki/Charles_Babbage">en.wikipedia</a></li>
<li class="pgdbetext"><a href="/ebooks/71292">The calculating engine</a> (English) (as Author)</li>
<li class="pgdbetext"><a href="/ebooks/4238">On the Economy of Machinery and Manufactures</a> (English) (as Author)</li>
<li class="pgdbetext"><a href="/ebooks/57532">Passages from the Life of a Philosopher</a> (English) (as Author)</li>
<li class="pgdbetext"><a href="/ebooks/1216">Reflections on the Decline of Science in England, and on Some of Its Causes</a> (English) (as Author)</li>
</ul>
Da. Lahko pa smo previdni in se premaknemo za toliko bratov, dokler
ne pridemo do ul
.
= link.parent.next_sibling
ul while ul.name != "ul":
= ul.next_sibling ul
Tako smo mimogrede spoznali še en atribut: name
. Vsak
element ima name
, ki vsebuje ime značke. link
,
recimo, je a
.
link.name
'a'
Tu gremo naprej po bratih, dokler ne pridemo do ul
. (Če
bi šlo zares, bi bil vaš izkušeni profesor, ki je doživel že veliko
hudega s stranih postavljalcev spletnih strani še malo bolj previden in
bi, za začetek, preverjal, da ul.name
slučajno ni že
h2
, kar bi pomenilo, da smo že pri naslednjem avtorju in da
ta, ki ga preiskujemo, nima objavljenih nobenih del.)
Sicer pa je iskanje najbližjega brata z določeno značko pogosta reč,
zato lahko gornjo zanko nadomestimo s preprostim klicem
find_next_sibling
:
= link.parent.find_next_sibling("ul")
ul ul
<ul>
<li class="pgdbxlink"><a href="https://en.wikipedia.org/wiki/Charles_Babbage">en.wikipedia</a></li>
<li class="pgdbetext"><a href="/ebooks/71292">The calculating engine</a> (English) (as Author)</li>
<li class="pgdbetext"><a href="/ebooks/4238">On the Economy of Machinery and Manufactures</a> (English) (as Author)</li>
<li class="pgdbetext"><a href="/ebooks/57532">Passages from the Life of a Philosopher</a> (English) (as Author)</li>
<li class="pgdbetext"><a href="/ebooks/1216">Reflections on the Decline of Science in England, and on Some of Its Causes</a> (English) (as Author)</li>
</ul>
Če želimo iskati nazaj, po prejšnjih bratih, kličemo
find_prev_sibling
.
Po tej ali po oni poti torej pridemo do ul
z deli
Babbagea.
Dela so znotraj elementov li
. li
vsebuje
a
in nizi znotraj tega so dela tega avtorja.
for li in ul.find_all("li"):
print(li.find("a").string)
en.wikipedia
The calculating engine
On the Economy of Machinery and Manufactures
Passages from the Life of a Philosopher
Reflections on the Decline of Science in England, and on Some of Its Causes
Tole je bil, jasno, šolski primer. Gutenbergove strani so prijazno preproste. V resnici imamo lahko z branjem veliko več zafrkavanja. Idejo pa smo videli.
Imena avtorjev vsebujejo tudi letnici rojstva in smrti. Vendar ne vsa. Kako izvleči letnico, da bi jo imeli shranjeno in bi z njo kaj koristnega počeli? Ali pa jo, recimo, izpustili ob izpisu.
Verjetno bi se dalo kaj načarati s split
-om, vendar bomo
tu pokazali drug, močnejši pristop. Letnica ima določeno obliko, vzorec,
ki ga je preprosto opisati: vsebuje nekaj števk, nato minus in še nekaj
števk. "Nekaj" je lahko tudi 0, če letnica ni znana. Če bi bil natančni,
bi dodali še, da so števke največ štiri, vendar ta omejitev najbrž ne bo
potrebna.
Da bi bil primer primerno zahteven, lahko števkam sledi še vprašaj, če glede letnice nismo prepričani in je bil možakar (ali možakarica) morda ojen ali umrl tudi kdaj prej ali kasneje.
Vzorec te oblike opišemo z regularnim izrazom
\d*\??-\d*\??
.
Res. Takole iz
"Tannenbaum, Samuel A. (Samuel Aaron), 1874?-1948"
izvlečemo, kdaj se je rodil in umrl Tannenbaum
import re
r"\d*\??-\d*\??", "Tannenbaum, Samuel A. (Samuel Aaron), 1874?-1948").group() re.search(
'1874?-1948'
Če čmo, pa lahko dobimo tudi vsako številko posebej.
= re.search(r"(\d*)\??-(\d*)\??", "Tannenbaum, Samuel A. (Samuel Aaron), 1874?-1948").groups() rojstvo, smrt
rojstvo
'1874'
smrt
'1948'
Je bil teaser v redu? Nas zanima? Potemtakem moramo najprej spoznati sintakso regularnih izrazov, potem pa še funkcije v Pythonu za delo z njimi.
Kar se bomo naučili tu, velja v splošnem, ne le v Pythonu. Regularni izraz so jezik za opis vzorcev, ki ga poznajo vsi splošno uporabni programski jeziki, z regularnimi izrazi pa lahko iščemo tudi po besedilih v urejevalnikih besedil.
Regularni izrazi so sestavljeni iz znakov, ki jih želimo poiskati (v
gornjem primeru je bil to samo -
) in znakov s posebnim
pomenom. Tule je (nepopoln) seznam:
.
: katerikoli znak\w
: katerakoli črka ali števka\d
: katerakoli števka\s
: bel prostor (presledek, tabulator, nova
vrstica)^
: začetek niza$
: konec niza.Znaki imajo tudi negacije: \W
je karkoli, kar ni črka
ali števka, \D
je karkoli, kar ni števka in \S
je karkoli, kar ni prazen prostor.
Poglejmo nekaj preprostih primerov
l.pa
je lahko lipa
ali lopa
,
lahko pa tudi lrpa
, ali tudi l1pa
,
l)pa
ali l pa
. Pika je lahko karkoli.l\wpa
je podoben, vendar izloči nealfanumerične znake -
med gornjimi primeri prepove l)pa
ali
l pa
.^l\wpa
bo ujel isto, vendar dodatno zahteva še, da se
podniz, ki se ujema z iskanim vzorcem, l\wpa
nahaja na
začetku preiskovanega niza.l.p.
je lipa
, lipe
,
lipi
, lipo
, pa tudi lopa
,
lope
, lopi
, lopo
in, seveda, tudi
lipr
, lrpr
ter celo l(p)
,
l)p^
in l-p
.l...
je vse, kar se začne z l
.....
je vsako zaporedje štirih znakov.Očitno sta tako .
kot \w
preveč liberalna:
dobro bi bilo, če bi lahko našteli znake, ki jih dovolimo. Za
l.pa
bi lahko dovolili e, i, o in u. Kadar želimo našteti
dovoljene znake, jih zapremo v oglate oklepaje:
l[eiou]pa
je lahko lepa
,
lipa
, lopa
ali lupa
. In nič
drugega.l[eiou]p[aeio]
pa je vse gornje v prvih štirih
sklonih.Če bi takoj za [
dali znak ^
, bi regularni
izraz lovil vse razen znakov v oklepajih. Torej črke in
števke, ki niso samoglasniki.
Kaj pa, lip
? Lahko povemo, da je nek znak lahko prisoten
ali pa tudi ne?
*
dovoli, da se reč pred zvezdico poljubnokrat
ponovi.+
prav tako, vendar hoče, da se pojavi vsaj
enkrat.?
pravi, da se sme pojaviti največ enkrat.Par primerov:
le+pa
je lepa
, leepa
,
leeepa
, leeeepa
ali še bolj lepa.le*pa
je lahko vse gornje, pa še lpa
zraven.lepa?
pa je lahko lepa
ali
lep
.Vsi trije, *
, +
in ?
se ne
nanašajo nujno pa črko.
l[eiou]+pa
je lahko tudi
leieoieoieoeiiioiepa
l.+pa
je lahko
l8umv9 jv &BT G#)GUJI pa
. Važno je, da se začne z
l
in konča s pa
, vmes pa je lahko čisto
karkoli - mora pa biti vsaj ena reč.Z okroglimi oklepaji lahko združujemo dele regularnega izraza v skupine.
l(ep)*a
je la
, lepa
,
lepepa
, lepepepa
...l([eiou]p)*a
je lahko, recimo
lepapapipopopopupa
.Kaj pa, kadar želimo, da izraz vsebuje piko? Torej zares piko, ne karkoli. Kako povemo, da s piko mislimo piko? Ali z vprašajem vprašaj? Z oklepajem oklepaj? Preden postavimo vzvratno poševnico.
l.p\.
je lep.
, lop.
,
l)p.
... Pika na koncu je zares pika.Kaj pa, če želimo vzvratno poševnico? V tem primeru napišemo dve poševnici. To že obvladamo - enako je bilo, če je kak trmoglav uporabnik Windowsov na vsak način hotel uporabljati vzvratne poševnice v opisih poti.
Zdaj se lahko vrnemo k regularnemu izrazu za letnice:
\d*\??-\d*\??
.
\d
hoče števko\d*
pravi, da je števk lahko tudi več. Morda celo
nobena.\d*\?
pravi, da mora slediti vprašaj (zaresen,
dobesedni vprašaj, saj smo predenj postavili \
)\d*\??
postavi vprašaj pod vprašaj: vprašaj lahko je,
lahko pa ga tudi ni.\d*\??-
: kot je rekel Freud, je cigara včasih samo
cigara; minus je tu samo minus.\d*\??-\d\??
: doda še vzorec za letnico smrti.findall
Začnimo z nizom.
= "Pred lipo je stala lepa lopa. Z lupo bi lahko iskal lepotne napake, a še tak lopov jih zlepa ne bi našel." s
Vse lope bi se lahko lotili iskati z
import re
"lop.", s) re.findall(
['lopa', 'lopo']
Lahko bi iskali tudi lepa
(lepo
,
lepi
...).
"lep.", s) re.findall(
['lepa', 'lepo', 'lepa']
"\Wlop.\W", s) re.findall(
<>:1: SyntaxWarning: invalid escape sequence '\W'
<>:1: SyntaxWarning: invalid escape sequence '\W'
/var/folders/2y/4j70c4q568l1j4lb6g1r0fk00000gn/T/ipykernel_90154/909440350.py:1: SyntaxWarning: invalid escape sequence '\W'
re.findall("\Wlop.\W", s)
[' lopa.']
"\Wlep.\W", s) re.findall(
<>:1: SyntaxWarning: invalid escape sequence '\W'
<>:1: SyntaxWarning: invalid escape sequence '\W'
/var/folders/2y/4j70c4q568l1j4lb6g1r0fk00000gn/T/ipykernel_90154/477483981.py:1: SyntaxWarning: invalid escape sequence '\W'
re.findall("\Wlep.\W", s)
[' lepa ']
Znebili smo se lopova, dobili pa neke presledke in pike. Pike? A ni
mišljeno, da pika ni pika, temveč ... Ja, brez skrbi. Tista pika v
vzorcu se je ujela z a-jem. \W
pa s piko v nizu. Tako kot
presledka. Ker smo rekli, da mora vzorec vsebovati tudi kaj, kar ni
črka, smo pač dobili še to.
Zdaj nas čaka kup detajlov. Prvi: bojte se vzvratnih
poševnic. Vzvratne poševnice imajo svoj pomen. Vemo,
\n
je znak za novo vrstico. \W
na našo srečo
nima posebnega pomena, sicer že gornje ne bi delalo.
Vsakič, ko v nizu v Pythonu uporabljamo vzvratne poševnice in hočemo
dobiti le vzvratne poševnice, jih moramo podvojiti. Torej ne
\Wlep.\W
temveč \\Wlep.\\W
. V regularnih
izrazih to postane zoprno, ker moramo včasih napisati dvojne poševnice v
izrazu (glej zgoraj) in ko vsako podvojimo, dobimo četverne poševnice.
Zato bomo regularne izraze raje pisali z r-nizi. Če pred narekovaj
dopišemo r
, bodo vzvratne poševnice samo vzvratne
poševnice. (Črka r
tu ne pomeni regular expression
temveč raw string; takšne nize lahko uporabljamo tudi drugje,
ne le v regularnih izrazih.) Pravilno je torej
r"\Wlep.\W", s) re.findall(
[' lepa ']
Tu ni razlike, v splošnem pa bo.
Kaj pa bomo storili s presledki? Preden se jih lotimo, opozorimo še na nekaj:
= "Lepa lopa je stala pod lipo."
s2
r"\Wlep.\W", s2) re.findall(
[]
Besede "Lepa" na začetku ni ujel, najprej, zato, ker je napisana z
veliko začetnico. Funkciji re.findall
lahko dodamo zastavice, s
katerimi spremenimo njeno delovanje in ena od njih je
re.IGNORECASE
.
r"^\Wlep.\W", s2, re.IGNORECASE) re.findall(
[]
Še vedno ni Lepa. Zdaj, očitno, zato, ker pred besedo ni nečesa, kar ni črka - preprosto zato, ker pred njo sploh ni ničesar. Vzorec bo potrebno preoblikovati tako, da bomo dovolili, da je pred besedo bodisi nečrka bodisi začetek niza.
Da ne doktoriramo iz vzorcev, preprosto prilepimo presledek na začetek in konec niza.
r"\Wlep.\W", " " + s2 + " ", re.IGNORECASE) re.findall(
[' Lepa ']
Zdaj pa se znebimo presledkov v najdenem nizu: če vzorec vsebuje
skupino, findall
ne bo vrnil celotnega podniza temveč le,
kar je v skupini.
r"\W(lep.)\W", s) re.findall(
['lepa']
Za konec polovimo še vse, kar je lepo lupa ali lupa.
r"\W(l[eiou]p[aieo])\W", s) re.findall(
['lipo', 'lepa', 'lupo']
Zdaj pa poskusimo tole: radi bi vse besede, ki se začnejo z lep-, lip-, lop- ali lup-.
r"\W(l[eiou]p\w*)", s) re.findall(
['lipo', 'lepa', 'lopa', 'lupo', 'lepotne', 'lopov']
Vzorec je zdaj takšen, da se mora začeti z nečem, kar ni črka (vendar
to ni del skupine), sledi l
, nato e
,
i
, o
ali u
, potem p
in potem poljubno število (lahko tudi 0) črk.
Kaj pa besede, ki vsebujejo l[eiou]p
, spredaj in zadaj
pa so lahko poljubne druge črke?
r"\w*l[eiou]p\w*", s) re.findall(
['lipo', 'lepa', 'lopa', 'lupo', 'lepotne', 'lopov', 'zlepa']
Požrešni operatorji
Glede tega vas je potrebno posvariti: kaj vrne l.*pa
?
Nepričakovano, tole: POVEJ za *?
r"l.*pa", s) re.findall(
['lipo je stala lepa lopa. Z lupo bi lahko iskal lepotne napake, a še tak lopov jih zlepa']
Python ima prav. Gre za niz, ki se začne z l
in konča s
pa
. Najbrž pa smo pričakovali krajšega, ne?
Operatorja *
in +
sta požrešna: vzameta,
kolikor je mogoče - vendar seveda tako, da je na konec še vedno mogoče
dodati pa
(ali karkoli že). Pogosto si predstavljamo, da
bosta vzela najkrajši možni niz. To dosežemo tako, da za
*
oziroma +
dodamo vprašaj. *?
in
+?
sta torej nepožrešni različici zvezdice in plusa.
r"l.*?pa", s) re.findall(
['lipo je stala lepa',
'lopa',
'lupo bi lahko iskal lepotne napa',
'lopov jih zlepa']
To seveda spet ni skladno s pričakovanji, vendar vidimo razliko:
začne pri l
in potem doda, kolikor je treba, da prida do
naslednjega pa
. Rezultat je sicer neželen, vendar vsaj
zabaven. :)
Gornjemu bi se seveda izognili, če bi .*?
zamenjali z
\w*
:
r"l\w*pa", s) re.findall(
['lepa', 'lopa', 'lepa']
Mimogrede pokažimo še, kako iz besedila izvlečemo vse besede (nekaj takega, vendar na bolj zapleten način, je zahtevala domača naloga Miklavževa pisma).
print(re.findall(r"\w+", s))
['Pred', 'lipo', 'je', 'stala', 'lepa', 'lopa', 'Z', 'lupo', 'bi', 'lahko', 'iskal', 'lepotne', 'napake', 'a', 'še', 'tak', 'lopov', 'jih', 'zlepa', 'ne', 'bi', 'našel']
Regularni izrazi imajo še veliko različnih opcij in trikov, zapletov in rešitev. V tem kratkem uvodu pač ne moremo našteti vseh; to bi v teoriji zahtevalo več predavanj, v praksi pa predvsem veliko izkušenj.
search
in
match
(in finditer
)Doslej smo se ukvarjali le z vzorci in iskanjem podnizov, ki jim ustrezajo. Z regularni izrazi v Pythonu je možno početi še veliko več. Vzorci bodo pogosto sestavljeni iz več delov, skupin, in običajno nas bo zanimala njihova vsebina.
Spomnimo se izraza, s katerim smo iskali letnice rojstev in smrti,
\d*\??-\d*\??
. Ta ima očitno dva dela, letnico rojstva in
letnico smrti. Lahko ju tudi uradno označimo kot skupini.
= "Tannenbaum, Samuel A. (Samuel Aaron), 1874?-1948"
author
r"(\d*)(\??)-(\d*)(\??)", author) re.findall(
[('1874', '?', '1948', '')]
Če vzorec vsebuje več skupin, findall
ne vrne seznama
nizov temveč seznam terk. To bi bilo dovolj, da bi lahko razbrali
letnico rojstva in smrti in - ker smo zvito zaprli v svojo skupino še
vprašaj, namesto da bi ga pridružili števkam - imamo še ločen indikator,
ki pove, ali je letnica zanesljiva ali ne.
Kaj, če bi hoteli spremeniti izpis - recimo odstraniti letnice iz
imena avtorja? findall
nam žal pove le, kaj je
našel, ne pa tudi kje. Po drugi strani pa: findall
vrne seznam, čeprav tule prav dobro vemo, da bo ta seznam vseboval
kvečjemu en element.
Za take primere sta uporabnejši metodi search
in
match
.
r"(\d*)(\??)-(\d*)(\??)", author) re.search(
<re.Match object; span=(38, 48), match='1874?-1948'>
r"(\d*)(\??)-(\d*)(\??)", author) re.match(
Tule, očitno, bolj search
. :) Razlika je v tem, da
match
zahteva, da se vzorec pojavi na začetku niza,
search
pa ne.
Rezultat, ki ga vrne search
, je re.Match
:
objekt, ki vsebuje več podatkov o tem, kaj je ujel vzorec.
= re.search(r"(\d*)(\??)-(\d*)(\??)", author) mo
Najprej: re.search
lahko vrne None
, če ne
najde ničesar. Klicu funkcije bo zato tipično sledil if
, ki
bo poskrbel za ta scenarij.
Metoda group
vrne vsebino posamezne skupine.
1) mo.group(
'1874'
3) mo.group(
'1948'
Ali več skupin.
1, 3) mo.group(
('1874', '1948')
Skupine začnemo šteti z 1
, ker 0
predstavlja celoten ujeti podniz.
0) mo.group(
'1874?-1948'
Ker je to natančno tisto, kar običajno potrebujemo, je 0
tudi privzeta vrednost. Celoten niz torej dobimo z
mo.group()
'1874?-1948'
Vse podskupine - tisto, kar bi vrnil findall
- pa dobimo
z groups
.
mo.groups()
('1874', '?', '1948', '')
Ker je skupin lahko veliko in jih je zoprno šteti, jih lahko tudi
poimenujemo. To sicer zaplete izraz, a poenostavi delo z njim. Skupine
namreč poimenujemo tako, da (...)
zamenjamo z
(?P<ime>...)
.
= re.search(r"(?P<born>\d*)(?P<bcertain>\??)-(?P<died>\d*)(?P<dcertain>\??)", author) mo
Zdaj lahko skupine naslavljamo z imeni, ne le indeksi.
"born") mo.group(
'1874'
"died") mo.group(
'1948'
Ali pa dobimo kar slovar skupin.
mo.groupdict()
{'born': '1874', 'bcertain': '?', 'died': '1948', 'dcertain': ''}
Za skupino lahko izvemo, kje v izvirnem nizu se začne oziroma konča. Ali oboje.
mo.start()
38
mo.end()
48
mo.span()
(38, 48)
Dodamo lahko indeks ali ime skupine.
"born") mo.span(
(38, 42)
Vzemimo zdaj našega Tannenbauma, shranimo letnici njegovega rojstva in smrti v ločeni spremenljivki ter ju odstranimo iz niza, tako da bo v njem ostalo le ime avtorja.
= "Tannenbaum, Samuel A. (Samuel Aaron), 1874?-1948"
author
= re.search(r", (?P<born>\d*)(?P<bcertain>\??)-(?P<died>\d*)(?P<dcertain>\??)", author)
mo
if mo:
= mo.group("born", "died")
born, died = author[:mo.start()]
author else:
= died = ""
born
author
'Tannenbaum, Samuel A. (Samuel Aaron)'
born
'1874'
died
'1948'
sub
in split
Nizi imajo metodo replace
, s katero zamenjamo določen
podniz z drugim. Regularnih izrazi imajo metodo sub
, ki
počne isto, le da niz, ki ga je potrebno zamenjati, podamo z regularnim
izrazom.
s
'Pred lipo je stala lepa lopa. Z lupo bi lahko iskal lepotne napake, a še tak lopov jih zlepa ne bi našel.'
"l.p.", "hišo", s) re.sub(
'Pred hišo je stala hišo hišo. Z hišo bi lahko iskal hišotne napake, a še tak hišov jih zhišo ne bi našel.'
To je seveda narobe. Lipo zamenjajmo s hišo, lepa pa bi bilo treba očitno zamenjati s heša. Storimo tako.
"l(.)p(.)", r"h\g<1>š\g<2>", s) re.sub(
'Pred hišo je stala heša hoša. Z hušo bi lahko iskal hešotne napake, a še tak hošov jih zheša ne bi našel.'
Preprosto: v vzorcu smo označili skupine in se v zamenjavi sklicali nanje.
Kaj pa tole: imamo niz, ki vsebuje neka števila in vsa bi radi povečali za 1. To zahteva nekaj računanja, zato bo potrebno napisati funkcijo.
def increase1(mo):
return str(int(mo.group()) + 1)
r"\d+", increase1, "Vzamemo 5 jajc in 6 litrov vode, pomešamo in dodamo 200 g sladkorja.") re.sub(
'Vzamemo 6 jajc in 7 litrov vode, pomešamo in dodamo 201 g sladkorja.'
In za konec še split
. Niz ima metodo split
,
ki prejme ločilo, po katerem naj deli niz. Ločilo je vedno le niz.
Regularni izrazi imajo metodo split
, ki prejme regularni
izraz. Razbijmo besedo glede na samoglasnike.
"[aeiou]", "samoglasniki") re.split(
['s', 'm', 'gl', 'sn', 'k', '']
"(?:[aeiou])", "samoglasniki") re.split(
['s', 'm', 'gl', 'sn', 'k', '']
(Medklic: če hočemo dobiti zloge - v smislu kombinacij črk, ki se
končajo s samoglasnikom, potem to ni split
temveč
findall
:
"[^aeiou]*[aeiou]", "samoglasniki") re.findall(
['sa', 'mo', 'gla', 'sni', 'ki']
Konec medklica.)
Na tem predavanju smo se bežno dotaknili teme, ki zahteva veliko znanja in izkušenj ter še več potrpljenja: pridobivanja podatkov iz dokumentov, ki v osnovi niso bili namenjeni temu, da bi iz njih (avtomatsko) pridobivali podatke. HTML so v osnovi namenjeni branju, prosta besedila pa sploh. Ker so strani HTML navadno oblikovane in ker so pogosto sestavljene avtomatsko (sploh, če podatki prihajajo iz baze), smemo upati, da bo oblikovanje dovolj pregledno in konsistentno, da bo mogoče na podlagi značk prepoznati dele, ki vsebujejo podatke, ki jih iščemo. Znotraj njih si bomo pogosto lahko pomagali z regularnimi izrazi.
Stran, ki smo jo videli tu, je preprosta. V praksi ne bo vedno tako. In, še huje, v praksi se strani spreminjajo in če bomo mukoma pripravili program za zajem podatkov z neke strani, nam nihče ne garantira, da bo že prihodnji teden vse drugače in začeti bo potrebno znova. Kadar je mogoče priti do podatkov v berljivi obliki na kak drug način, je to vedno boljša opcija. Sicer pa - veliko sreče. :)