Regularni izrazi

Ena najbolj uporabnih stvari v skriptnih jezikih so "regularni izrazi". Če jih boste znali, boste glavni frajerji in boste lahko delali odlične reči. Po drugi strani pa niso prav nič zapleteni, čisto enostavno se jih je naučiti ... in, no, priznam, kar zoprno sestavljati. Ko sestavljamo kaj bolj zapletenega, se tudi stari mački nekoliko zafrkavamo, preden jih spravimo v delujoče stanje.

Regularni izraz

Če bi hoteli teoretizirati, bi rekli, da opisujejo jezik, pri čemer z "jezikom" mislimo nekakšno množico nizov. So tudi v tesnem sorodu s končnimi avtomati, ki vam morda niso več neznani, če kaj sledite Uvodu v računalništvo.) A pustimo teorijo in uvode. Naučili se bomo pisati nize, s katerimi opišemo nekakšne vzorce. Recimo, da imamo na voljo naslednje oznake, ki jih lahko tlačimo v nize:

.
katerikoli znak
\w
katerakoli črka ali števka
\W
katerikoli znak, ki ni črka ali števka
\d
katerakoli števka
\D
katerakoli znak, ki ni števka
\s
beli prostor (presledek, tabulator, nova vrstica...)
\S
karkoli, kar ni beli prostor
^
začetek niza
$
konec niza
<
\
vzvratno poševnico uporabimo, ko bi radi v vzorec vpisali, recimo, piko. Znak . ima namreč poseben pomen (glej zgoraj), zato takrat, kadar v resnici želimo piko, napišemo \.. Enako velja za zvezdico, vprašaj, plus... in tudi vzvratno poševnico.
oglati oklepaji
mednje napišemo seznam znakov in opisujejo katerikoli znak s tega seznama; če je prvi znak ^, pa to pomeni katerikoli znak, ki ni na tem seznamu. Znotraj oglatih oklepajev je pika samo pika in celo vzvratna poševnica samo vzvratna poševnica.
Poglejmo nekaj preprostih primerov vzorcev
l.pa
Ta izraz opisuje "l", ki mu sledi karkoli in nato "pa". To je lahko "lipa", "lepa" ali "lopa", pa tudi "lrpa", "lmpa", "lgpa". In celo "l3pa", "l/pa" in "l pa". Vzorcu pa ne ustrezata, recimo, "sipa", saj se ne začne z "l" ali "lotupa", saj ima med "l" in "pa" več kot eno samo črko.
l.p.
Tole pa je lahko tudi "lipi" ali "lopi" ali "lspt" ali "l1p4" ali "l-p)"... Torej vse besede s štirimi znaki, pri čemer mora biti prvi znak "l", tretji pa "p".
l\wp\w
Ta vzorec je podoben prejšnjemu, le da zahteva, da sta drugi in četrti znak črki ali številki. "lipa" in "l1p4" sta še vedno spremenljivi, "l-p)" pa ne več.
lip[aieo]
Tole pa je lahko "lipa", "lipi", "lipe" ali "lipo". Ne pa "lipu" ali "lipm".
lip[^u t]
Ta vzorec opisuje "lip", ki mu sledi katerikoli znak, razen u-ja, presledka ali t-ja.
s[tpr]anje
"stanje", "spanje" in nekatere druge reči.
Zdaj pa še par posebnih znakov.
okrogli oklepaji
Z okroglimi oklepaji označimo skupino
*
Z zvezdico označimo, da se sme prejšnji znak ali prejšnja skupina (ki smo jo zaprli v okrogle oklepaje) poljubnokrat ponoviti - lahko tudi ničkrat.
+
Plus pomeni isto kot zvezdica, le da zahteva, da se prejšnja skupina ali znak pojavi vsaj enkrat.
?
Z vprašajem povemo, naj se prejšnja skupina pojavi enkrat ali nobenkrat.
Spet si oglejmo nekaj primerov.
Go*l!
Ta vzorec opisuje nize, kot so "Gol!", "Gooooooool!" in "Goooooooooooooooool!". Žal pa tudi "Gl!". :)
Go+l!
Popravljena verzija gornjega: zahteva, da se "o" pojavi vsaj enkrat.
Go+l?!
Tole pa je podobno kot zgoraj, le da se črka l lahko pojavi ali pa tudi ne. To je torej lahko karkoli od tega, kar smo našteli zgoraj, poleg tega pa tudi "Go!" ali "Gooooooooooo!"
Pa še par bolj zapletenih.
(brm)+
Opisuje motor, ki dela "brmbrmbrmbrmbrmbrm" ali "brmbrmbrmbrmbrmbrmbrmbrm" ali vsaj "brm".
ra(ta)*
Opisuje mitraljez (ki je lahko tudi pokvarjen in reče samo "ra" in razpade)
tr([aeiou]l)*[aeiou]
Opisuje prepevanje, ko ne poznamo besedila ("tralalelalalalilolololu")
jodl(dodl)*dii
Jodlanje (in jodldodlanje in jodldodldodldodldodlanje).
jodl(dodl)?dii
Tole pa je samo jodlanje in jodldodlanje, ne pa tudi jodldodldodldodldodlanje.
Pri plusu, zvezdici in vprašaju nas včasih zmoti, da požrejo preveč. Če hočemo, da požrejo čim manj, jim dodamo vprašaj.
*?, +?, ??
Če k zvezdici, plusu ali vprašaju dodamo še en vprašaj, zahtevamo, naj ta zvezdica, plus ali vprašaj "požre" čim manj znakov. Tole je na prvi pogled zelo praktično; v resnici bo začetnik, ko odkrije te vprašaje, naredil veliko naivnih napak, zato se jim, vsaj v začetku, raje izognite in poskusite to, kar želite povedati, povedati brez njih.
Vzemimo stavek "Ta nas Janez je en dolgocasnez, prav res". Če poskusimo v tem stavku poiskati podniz, ki ustreza vzorcu "J.*nez", bomo izvedeli, da "Janez je en dolgocasnez", kar ni ne lepo ne prav in nikakor ni tisto, kar smo hoteli. Težava je v tem, da je zvezdica požrla "nez je en dolgocas"; izraz namreč pravi, da hočemo "Ja", potem (karseda veliko) drugih črk in potem "nez". Če pa poiščemo "J.*?nez", bomo dobili samo "Janez", saj bo ".*" požrlo le toliko znakov, kolikor jih potrebujemo, da je iskanje uspešno, torej, v tem primeru, le črko "a".

Funkcije za uporabo regularnih izrazov

Naučili smo se opisovati vzorce. Kaj pa lahko s temi opisi počnemo?

Regularni izrazi, ki smo jih spoznali, so bolj ali manj enaki v vseh jezikih. Jeziki, ki imajo veliko opraviti z iskanjem po besedilih, na primer php in javascript, imajo regularne izraze tesno vdelane v jezik - tako kot ima Python vdelana števila in nize (za nize nimamo "posebnega modula", ki bi ga morali uvoziti, preden lahko delamo z njimi), imajo ti jeziki vdelane tudi regularne izraze. Ne da bi potrebovali poseben modul, lahko preverimo, ali ima določen niz določeno obliko, podano z regularnim izrazom. Python je splošnejši jezik, zato regularnih izrazov ne sili v ospredju, temveč so lepo v posebnem modulu. Imenuje se re, kar je okrajšava za regular expression.

Za primer uporabimo rdečo nit predavanj izpred nekaj let: v besedilu iščemo imena potencialnih teroristov, pri čemer so nam sumljive kar vse besede, ki se začnejo z veliko črko. Besedilo nekega prestreženega elektronskega sporočila je bilo takšno:

msg = """Dragi Ahmed, kako si kaj? Upam, da so otroci ze zdravi. Mustafa, Osama in jaz smo se sli danes malo razgledat in kaze kar dobro. Abdulah pa ni mogel zraven, je sel v Pesavar prodat se tri kamele. Osama sicer pravi, da se mu to pred zimo ne splaca, ampak saj ves, kaksen je Abdulah. Harun in on, nic jima ne dopoves, se Osama ne. Jibril me klice, moram iti; oglasi se kaj na Skype! tvoj Husein PS, oprosti za sumnike - rashmeD nas je prepricuje, naj gremo na Python 3.0, ampak za zdaj smo se kar na ta starem. """

Vzorec, ki bo predstavljal ime - nekaj, kar se začne z veliko črko in nadaljuje s vsaj eno črko, je [A-Z]\w+.

Najpreprostejša za uporabo (a ne tudi najbolj uporabna) funkcija, na kateri lahko poženemo ta izraz, je findall, ki, kot bi bili ob imenu nemara presenečeni (posebej, če ne bi znali angleško) poišče vse (neprekrivajoče se) podnize, ki jim ustreza izraz. Če imamo sporočilo shranjeno v nizu msg, bomo seznam imen dobili z

>>> re.findall(r"[A-Z]\w+", msg) ['Dragi', 'Ahmed', 'Upam', 'Mustafa', 'Osama', 'Abdulah', 'Pesavar', 'Osama', 'Abdulah', 'Harun', 'Osama', 'Jibril', 'Skype', 'Husein', 'PS', 'Python']

Opazka: pred narekovaj sem dodal r. Iz davnega začetka semestra se morda spomnite, kakšne posledice ima to: vse vzvratne poševnice (po domače: backslashi) po tem pomenijo le vzvratne poševnice, ne ubežnih zaporedij (torej, \n pomeni vzvratno poševnico in n, ne pa prehoda v novo vrsto). V regularnih izrazih se bo vedno trlo vzvrtanih poševnic, medtem ko bodo nove vrste in tabulatorji strašno redki, zato bomo skoraj vedno uporabljali nize z r-jem pred narekovaji.

Metoda findall je najpreprostejša, ker vrne kar (pod)nize, ki ustrezajo vzorcu. Druge funkcije vračajo objekt, iz katerega lahko razberemo še kaj več. Vzemimo search, ki poišče prvo pojavitev vzorca.

>>> mo_ime = re.search(r"[A-Z]\w+", msg) >>> mo_ime <_sre.SRE_Match object at 0x045812F8>

mo_ime ni niz, tako kot prej, temveč objekt, ki vsebuje podatke o podnizu, ki se ujema z izrazom. (Ker gre za "Match object", zato sem mu pripel mo_. To spet spominja na madžarsko notacijo, ki sem jo omenil pri Qtju in tudi tu izvira iz (mojih) izkušenj: če imaš spremenljivko, ki predstavlja objekt, ki se nanaša na najdeno ime, boš imel najbrž tudi spremenljivko, ki bo vsebovala niz z najdenim imenom in imeli bosta tendenco, da se bosta enaki. Če prvemu vedno pripnemo mo_, je problem rešen.

) >>> mo_ime.group() 'Dragi' >>> mo_ime.start() 0 >>> mo_ime.end() 5 >>> mo_ime.span() (0, 5)

Tule start pove indeks črke v nizu, kjer se ujemanje začne in end indeks črke za ujetim podnizom; span pove oboje hkrati. Najdeni niz se torej nahaja v msg[0:5]. Razlog, da se metoda, ki vrne celotni podniz, imenuje group(), pa bo postal jasen malo kasneje, ko se bomo pogovarjali o skupinah.

Če želimo poiskati vse pojavitve vzorca in jih obdelati v zanki, bomo uporabili finditer, kot recimo tule:

for s in re.finditer(r"[A-Z]\w+", msg): print(s.group())

Metoda sub je podobna metodi replace, ki smo jo spoznali pri nizih: z njo lahko zamenjamo vse pojavitve danega vzorca s čim drugim.

>>> print(re.sub(r"[A-Z]\w+", "XXX", msg)) XXX XXX, kako si kaj? XXX, da so otroci ze zdravi. XXX, XXX in jaz smo se sli danes malo razgledat in kaze kar dobro. XXX pa ni mogel zraven, je sel v XXX prodat se tri kamele. XXX sicer pravi, da se mu to pred zimo ne splaca, ampak saj ves, kaksen je XXX. XXX in on, nic jima ne dopoves, se XXX ne. XXX me klice, moram iti; oglasi se kaj na XXX! tvoj XXX XXX, oprosti za sumnike - rashmeD nas je prepricuje, naj gremo na XXX 3.0, ampak za zdaj smo se kar na ta starem.

V zvezi s sub lahko opazimo, da so argumenti funkcije obrnjeni nekoliko drugače kot pri replace: replace je metoda razreda str, torej je niz, katerega podnize želimo spreminjati že implicitno podan (kot self), povedati je potrebno le, kakšen podniz zamenjamo s katerim. Za primer: na nekaterih operacijskih sistemih so presledki v imenih datotek precej tečna reč. Kako bi se jih znebili? Če bi poznali le nize, bi rekli

fname = fname.replace(" ", "_") Metodi sub pa kot argument povemo vzorec, niz, po katerem naj išče in s čim naj zamenjuje. Če bi želeli gornjo zamenjavo opraviti z regularnimi izrazi, bi napisali: fname = re.sub(" ", "_", fname)

Regularni izrazi so za tako preprosto zamenjavo prevelik kanon. sub namreč zmore še veliko več; namesto niza, s katerim naj zamenja ujemajoči se podniz, ji lahko podamo kar funkcijo, ki naj jo pokliče ob vsakem ujemanju: funkcija bo kot argument dobila Match object, kot rezultat mora vrniti niz, ki naj zamenja ujemajoči se podniz. Podniz lahko zamenjamo z, recimo, enakim podnizom, vendar zapisanim z velikimi črkami.

>>> def velike_crke(mo): ... return mo.group().upper() ... >>> print re.sub(r"[A-Z]\w+", velike_crke, msg) DRAGI AHMED, kako si kaj? UPAM, da so otroci ze zdravi. MUSTAFA, OSAMA in jaz smo se sli danes malo razgledat in kaze kar dobro. ABDULAH pa ni mogel zraven, je sel v PESAVAR prodat se tri kamele. OSAMA sicer pravi, da se mu to pred zimo ne splaca, ampak saj ves, kaksen je ABDULAH. HARUN in on, nic jima ne dopoves, se OSAMA ne. JIBRIL me klice, moram iti; oglasi se kaj na SKYPE! tvoj HUSEIN PS, oprosti za sumnike - rashmeD nas je prepricuje, naj gremo na PYTHON 3.0, ampak za zdaj smo se kar na ta starem.

Da ne definiramo brez potrebe takšnih kratkih funkcij, si lahko pomagamo z lambda-funkcijami. O njih se pri Programiranju 1 nismo pogovarjali; primeren trenutek je bil kvečjemu prejšnji teden, saj je njihovo naravno mesto funkcijsko programiranje. Tule jih bomo samo uporabili, v izziv radovednim:

print(re_ime.sub(lambda mo: mo.group().upper(), msg)) Za primer obrnimo vsa imena po arabsko, z desne na levo: >>> print(re.sub(r"[A-Z]\w+", lambda mo: "".join(reversed(mo.group())), msg)) ... igarD demhA, kako si kaj? mapU, da so otroci ze zdravi. afatsuM, amasO in jaz smo se sli danes malo razgledat in kaze kar dobro. haludbA pa ni mogel zraven, je sel v ravaseP prodat se tri kamele. amasO sicer pravi, da se mu to pred zimo ne splaca, ampak saj ves, kaksen je haludbA. nuraH in on, nic jima ne dopoves, se amasO ne. lirbiJ me klice, moram iti; oglasi se kaj na epykS! tvoj niesuH PS, oprosti za sumnike - rashmeD nas je prepricuje, naj gremo na nothyP 3.0, ampak za zdaj smo se kar na ta starem.

Še ena (in zadnja, za danes) zanimiva funkcija, ki jo nudijo regularni izrazi, je split. Tej smo doslej podajali točen podniz, po katerem naj deli. Tako bo tudi poslej, modul za regularne izraze pa ima še eno, drugo funkcijo split, ki ji povemo vzorec, po katerem naj deli. Lahko, recimo, rečemo, naj sporočilo razdeli ob vsakem imenu (kar ni posebej uporaben zgled, je pa preprost).

>>> re.split(r"[A-Z]\w+", msg)
['', ' ', ',\n\nkako si kaj? ', ', da so otroci ze zdravi.\n\n', ', ', ' in jaz smo se sli danes malo razgledat in\nkaze kar dobro. ', ' pa ni mogel zraven, je sel v ', '\nprodat se tri kamele. ', ' sicer pravi, da se mu to pred\nzimo ne splaca, ampak saj ves, kaksen je ', '. ', ' in\non, nic jima ne dopoves, se ', ' ne. ', ' me klice,\nmoram iti; oglasi se kaj na ', '!\n\ntvoj ', '\n\n', ', oprosti za sumnike - rashmeD nas je prepricuje, naj gremo na ', ' 3.0, ampak za zdaj smo se kar na ta starem.\n']

V zvezi z Match objecti so nam ostale še skupine. Kaj so in kaj počnemo z njimi, pa bomo videli na primerih.

Prevedeni regularni izrazi

V davnih časih smo se pri predmetih, kot je Uvod v računalništvo, učili Moorevoih in Mealyjevih avtomatov. Danes so menda iz mode, zato bo moral ta, ki ga zanima, kako delujejo regularni izrazi, najbrž počakati do Prevajalnikov in navideznih strojev, ki si jih lahko izbere v tretjem letniku. Za zdaj vam lahko povem le, da se izraz, preden lahko računalnik v resnici naredi karkoli z njim, prevede v nekaj, čemur se reče končni avtomat, ta pa je neke vrste usmerjen graf. (Tako da boste vsaj vedeli, čemu služi to, kar počnete pri Diskretnih strukturah.)

Za razumevanje tega, kar je pomembno tu, je bistveno le: regularni izraz se mora v nekaj prevesti. Če napišete program, ki vsebuje nekaj takšnega:

for i in range(10000): re.findall(r"[A-Z]\w+", msg[i]) # in potem še kaj naprej bo moral nesrečni računalnik deset tisočkrat prevesti regularni izraz [A-Z]\w+ v tisto, v kar ga pač prevaja. Pri tako kratkem in preprostem izrazu še gre, pri čem bolj zapletenem pa bi program tekel bistveno hitreje, če bi ga lahko prevedli enkrat za vselej. In res ga lahko.

re_ime = re.compile(r"[A-Z]\w+") for i in range(10000): re_ime.findall(msg[i]) # in potem še kaj naprej Spremenljivka re_ime (spet madžarska notacija!) zdaj predstavlja prevedeni izraz. Objekt re_ime ima vse funkcije, ki smo jih poznali prej, le da gre zdaj pač za metode objekta in ne funkcije modula: namesto re.findall, re.finditer, re.sub, re.split, zdaj pišemo re_ime.findall, re_ime.finditer, re_ime.sub, re_ime.split. Poleg tega pa še izpustimo prvi argument, vzorec, saj je izraz že pripravljen.

Sam vedno eksplicitno pokličem prevajanje izraza, preden ga uporabim; tako sem se navadil in škoditi ne more. Tule pa bomo počeli, kakor bo naneslo.

Primeri

Primer: teroristi

Rešimo, za začetek, kar staro nalogo, ki je zahtevala, da izpišemo, kolikokrat se v sporočilu pojavi posamezno ime.

import re imena = {} for ime in re.findall(r"[A-Z]\w+", msg): imena.setdefault(ime, 0) imena[ime] += 1 for ime, stevec in imena.items(): print("{} {}".format(ime, stevec))

Gre pa, jasno, tudi hitreje: modul collections ima razred Counter, ki prešteje število ponovitev v poslanem seznamu, slovarju, množici... Takole:

>>> collections.Counter(re.findall(r"[A-Z]\w+", msg)) Counter({'Osama': 3, 'Abdulah': 2, 'PS': 1, 'Ahmed': 1, 'Mustafa': 1, 'Python': 1, 'Pesavar': 1, 'Upam': 1, 'Dragi': 1, 'Harun': 1, 'Husein': 1, 'Skype': 1, 'Jibril': 1})

To naš program precej poenostavi, saj nas odreši glavnega opravila, štetja.

import re, collections for ime, stevec in collections.Counter(re.findall(r"[A-Z]\w+", msg)).items(): print("{} {}".format(ime, stevec))

Primer: branje vseh slik z določene strani

Tisti, ki vsaj malo poznate HTML, veste, da so slike na spletnih straneh praviloma postavljene tako, da HTML vsebuje značko IMG, ki videti nekako tako:

<img src="ime_slike.gif"/> Z malo potencialnimi komplikacijami, seveda. Oklepaju sicer vedno sledi najprej img, a poleg src lahko naletimo še na kaj drugega. Lepo vzgojen HTML ima poleg src vsaj še alt, slabo vzgojen pa tudi onclick ali kaj hujšega. Poleg tega so lahko kjerkoli, razen sredi besed, tudi presledki.

Kar iščemo, lahko z besedami opišemo kot oklepaj (namreč tale: <), ki mu lahko sledi kaj praznega prostora, nato pa img. Potem sme biti čisto karkoli razen zaklepaja, > in nekoč mora priti na vrsto src, pred njim pa mora biti kak znak, ki ni črka (če bi, recimo, pisalo mesrc, to ni OK, če je pred src presledek, tabulator, nova vrsta ali, zaradi mene tudi narekovaj ali vejica (čeprav to ni povsem po predpisih) pa je OK. Besedi src mora slediti enačaj, pred in za njim sme biti bel prostor. Nato pa narekovaj, za katerim pride tisto, kar nas v resnici zanima: URL slike. Ta je sestavljen iz poljubnih znakov razen narekovajev... in na koncu pride še narekovaj.

Z regularnim izrazom taisto povemo tako:

<\s*img[^>]*\Wsrc\s*=\s*"([^"]*)" Se pravi
<oklepaj, <
\s*poljubno praznega prostora (lahko tudi nič)
imgbeseda img
[^>]*poljubno dolga poljubna šara, razen zaključnega oklepaja
\Wkarkoli, kar ni črka
srcbeseda src
\s*poljubno praznega prostora (lahko tudi nič)
=enačaj
\s*poljubno praznega prostora (lahko tudi nič)
"narekovaj
([^"]*)poljubne reči razen narekovaja; čemu oklepaji, bomo še videli
"narekovaj
Ko smo rekli poljubno dolga poljubna šara, razen zaključnega zaklepaja: kdo nam jamči, da ta "poljubna šara" ne bo požrla tudi srcja in URLja? To nam jamči sam regularni izraz! Če bi [^>]* požrl src, potem se regularni izraz ne bi mogel ujemati s sicer pravilno vstavljeno sliko. Regularni izraz (oziroma knjižnica, ki ga poganja) pa vedno naredi vse, da bi se izraz ujemal, torej tudi ukroti požrešne dele.

Podobno, a ravno obratno vprašanje je, kdo zagotavlja, da bo ([^"]*), to so, poljubne reči razen narekovaja, res požrle ves URL in ne odnehale, tako, za hec, po treh znakih? Tako kot prej, nam tudi to zagotavlja preostanek izraza. V opisu regularnega izraza delu ([^"]*) namreč sledi "; če ([^"]*) ne požre vsega do narekovaja, se tisto, kar sledi, ne bo ujemalo z narekovajem, kot zahteva izraz.

Je kdo opazil, da nikjer ne lovimo zaključnega zaklepaja, >. Res je, ne omenjamo ga. Z razlogom. Vseeno nam je zanj. Ko ulovimo URL, nam je za vse, kar se dogaja naprej, vseeno. Da bo še jasneje, pripnimo h gornji tabeli še en preprost in en zapleten podniz, ki se ujame v izraz. Najprej<img src="ime_slike.gif"/>

<<
\s*
imgimg
[^>]*
\W(presledek med img in src)
srcsrc
\s*
==
\s*
""
([^"]*)ime_slike.gif
""

Pa še bolj zapleten primer: kako regularni izraz zgrabi < img alt="Tvoj brskalnik ne kaže slik" onclick="http://www.fri.uni-lj.si" src= "x.gif" class="psl" >

<<
\s*(presledka pred img)
imgimg
[^>]* alt="Tvoj brskalnik ne kaže slik" onclick="http://www.fri.uni-lj.si" (pred src so trije presledki; "šara" pograbi prva dva)
\W(tretji presledek pred src)
srcsrc
\s*
==
\s*(presledek)
""
([^"]*)x.gif
""

Zdaj pa uporabimo izraz v funkciji download_images, ki bo kot argument sprejela URL strani in ciljni direktorij. Prebrala bo s stran, nato pa z nje snela vse slike in jih shranila v ciljni direktorij.

import re import urllib.request import os re_img = re.compile(r'<\s*img[^>]*\Wsrc\s*=\s*"([^"]*)"', re.DOTALL + re.IGNORECASE) def download_images(url, target_dir): if not os.path.exists(target_dir): os.mkdir(target_dir) elif not os.path.isdir(target_dir): raise IOError("'%s' is not a directory" % target_dir) page = urllib.request.urlopen(url).read().decode("utf-8") for mo in re_img.finditer(page): img_url = mo.group(1) img = urllib.request.urlopen(img_url).read() img_name = os.path.split(img_url)[1] target_name = os.path.join(target_dir, img_name) open(target_name, "wb").write(img)

Regularni izraz smo tokrat enkrat za vselej prevedli pred funkcijo in ga shranili v re_img. Dodali smo še en argument, zastavice, s katerimi povemo, naj vzorec . požira tudi znake za novo vrstico, \n (re.DOTALL) in da nam je vseeno za male in velike črke (re.IGNORECASE), tako da se bo, denimo, img ujemal tudi z IMG, Img in iMg...

Funkcija nato preveri ali ciljni direktorij obstaja; če ga ni, ga naredi, če je, pa preveri, ali je v resnici direktorij in v nasprotnem primeru - kot znamo od prejšnjič - sproži izjemo.

Sledi pravo delo: poberemo podano stran. Kar dobimo z read, ni niz (str) temveč bajti (bytes). Ker regularni izrazi brskajo le po nizih, pretvorimo bajte v niz. Če bi hoteli narediti, kot se šika, bi ugotovili, kako je stran v resnici kodirana, tule pa bomo predpostavili, da gre za utf-8 in poklicali decode("utf-8") Potem poiščemo vse pojavitve značke img. Zdaj pride, kar sem zamolčal: group. Ko smo v prejšnjih primerih želeli iz match objecta dobiti podniz, ki se je prilegal regularnemu izrazu, smo poklicali group(). Zdaj bi želeli priti do podniza, ki se prilega URLju slike, torej tistemu med narekovaji za srcem. Zato smo ta del zaprli v oklepaje in s tem naredili novo skupino. Do njene vsebine pridemo z group(1). Čemu 1? Zato pač, ker gre za prvo skupino. Če bi jih imeli več, bi jih lepo prešteli in povedali, ali želimo vsebino tretje ali šeste.

"Lepo prešteli"? Tole štetje ni prav nič lepo in prikladno. Regularni izrazi so zapleteni in kaj lahko se uštejemo. Še huje: ko izraz spremenimo, bomo morda dodali novo skupino in vse bo treba preštevilčiti. Zato je skupine bolj praktično poimenovati. Namesto

re_img = re.compile(r'<\s*img[^>]*\Wsrc\s*=\s*"([^"]*)"', re.DOTALL + re.IGNORECASE) napišemo re_img = re.compile(r'<\s*img[^>]*\Wsrc\s*=\s*"(?P<url>[^"]*)"', re.DOTALL + re.IGNORECASE) in skupina je dobila ime, url. Do vsebine te skupine potem pridemo z mo.group("url").

Sliko poberemo v niz z urllib.request.urlopen. Zdaj pa jo je potrebno še shraniti. URL slike bo, recimo, takšen: http://i1.nyt.com/images/2011/01/01/arts/design/01moth_costa/01moth_costa-moth.jpg in takšno sliko želimo shraniti pod imenom 01moth_costa-moth.jpg. Da pridemo do njega, lahko za prvi približek posilimo kar funkcijo os.path.split(p), ki kot argument (običajno) prejme datoteko skupaj s potjo do nje, na primer, c:\Users\Janez\Desktop\desktop.ini, kot rezultat pa vrne terko s potjo in datoteko, na primer ("c:\Users\Janez\Desktop", "desktop.ini"). URL je dovolj podobna reč, da ga bo os.path.split pravilno razcepil.

Funkcija deluje, lahko jo preskusimo. Ker vaje ni nikoli preveč, pa poskusimo spremeniti izraz tako, da se izognemo klicu os.path.join. Pot vodi prek razmisleka, da je URL sestavljen iz dveh delov; prvi del je pot, drugi ime datoteke in slednji ne vsebuje poševnic, /. (Tole je le zmerno res: pri, denimo, slikah, ki se generirajo sproti, URL navadno ne bo vseboval imena datoteke. Ta detajl za zdaj pozabimo.) Ime, torej tisto med narekovaji, bi zato spremenili iz [^"]*, karkoli razen narekovaja, v [^"]*[^"/]*, to je, karkoli razen narekovaja, ki mu sledi karkoli razen narekovaja in poševnice. No, tole še ne bo povsem dobro, saj prvemu delu nič ne preprečuje, da ne bi kar vedno požrl celotnega imena in za drugi del ne bi ostalo nič. Prva rešitev, ki nam pride na misel, je [^"]*/[^"/]*, torej, karkoli razen narekovaja, ki mu sledi poševnica in potem karkoli razen narekovaja in poševnice. Tudi to ne bo povsem dobro, saj zdaj zahtevamo, da URL vsebuje poševnico in zdaj ne bomo več prepoznali povsem legalne slike <img src="ime_slike.gif"/>. Prava rešitev je, da prvemu delu pač zapovemo, naj požre čim manj, tako da zvezdici dodamo vprašaj, [^"]*?[^"/]*. Zdaj vse deluje, kot mora: prvi del lahko požira karkoli (vendar čim manj), drugi pa ne mara poševnic. Če obstaja (vsaj ena) poševnica, bo prvi del požrl vse do (zadnje) poševnice in tako omogočil drugemu, da se ujame (a požrl ne bo nič več, kot je nujno treba, saj požira čim manj). Če pa poševnice sploh ni, bo vse požrl drugi del, saj prvemu ni potrebno požreti ničesar.

In zdaj določimo in poimenujmo še skupine. Dve bosta in ena bo del druge. Najprej poskrbimo za ime datoteke, se pravi to, kar sledi poševnici: [^"]*?(?P[^"/]*). Vse skupaj pa zapremo še enkrat, saj celoten URL vsebuje tako prvi del kot tudi ime: (?P[^"]*?(?P[^"/]*)). Funkcija s takšnim regularnim izrazom je taka:

re_img = re.compile(r'<\s*img[^>]*\Wsrc\s*=\s*"(?P<url>[^"]*?(?P<fname>[^"/]*))"', re.DOTALL+re.IGNORECASE) def download_images(url, target_dir): if not os.path.exists(target_dir): os.mkdir(target_dir) elif not os.path.isdir(target_dir): raise IOError("{}' is not a directory".format(target_dir)) page = urllib.request.urlopen(url).read().decode("utf-8") for mo in re_img.finditer(page): img_url, img_name = mo.group("url", "fname") img = urllib.request.urlopen(img_url).read() target_name = os.path.join(target_dir, img_name) open(target_name, "wb").write(img)

Primer: branje telefonskega imenika FRI

Na strani s seznamom osebja FRI vsaki osebi ustreza vrstica podobna tej

<tr> <td class="lablinksName"> <div> <a href="/si/ivan-bratko/default.html"> dr. Ivan Bratko akad. prof. </a> </div> </td> <td class="lablinksMail"> <a href="javascript:void(cmPopup('sendMessage', '/si/ivan-bratko/mailer.html', true, 350, 350));"> <img src="/Static/images/gui/mail.gif" height="8" width="11"> </a> </td> <td class="lablinksPhone"> <div> <strong>T:</strong> +386 1 4768 393 </div> </td> </tr> V resnici je vse stlačeno v eno samo vrsto, v lepo hierarhično obliko sem jo (za tiste, ki znajo brati HTML - kdor ne zna, naj se le čimprej nauči, računalnikar mora to pač znati!) razpisal sam.

Ob tem je možnih tudi nekaj izjem:

  • V celici z imenom imajo nekateri za </a> še dodatek z njihovo funkcijo, na primer <br /><em>prodekanja za pedagoško dejavnost</em>; tega bomo ignorirali
  • Pri tistih, ki nimajo objavljene telefonske številke bomo namesto ustrezne celice našli <td class="lablinksPhone"><div> </div></td>
  • Pri tistih, ki nimajo spletne strani, manjka oznaka, kot je <a href="/si/ivan-bratko/default.html"> in pripadajoči zaključek <a>
Najsiromašnejša oblika je tako <tr> <td class="lablinksName"> <div> dr. Marko Colnar </div> </td> <td class="lablinksMail"> </td> <td class="lablinksPhone"> <div> </div> </td> </tr>

Regularni izraz, s katerim ulovimo opis osebe - krajši ali daljši -, je

<tr>začetni <tr>
.*?vse do prvega <div>
<div>prvi <div>
(povezava na spletno stran, če jo oseba ima
\s*morebitni prazni prostor pred imenom
(?P[^<]*)ime - vse do prvega znaka <
.*?šara pred telefonom
<td class="lablinksPhone">začetek polja s telefonom
(<div><strong>T:</strong>\s*(?P<telefon>[^<]*)\s*</div>)?telefon
ali, na kratko r'<tr>.*?<div>(<a href="(?P<url>[^"]*)">)?\s*(?P<ime>[^<]*).*?<td class="lablinksPhone">(<div><strong>T:</strong>\s*(?P<telefon>[^<]*)\s*</div>)?') Tako v URLju kot v telefonu smo naredili dve skupini. Zunanja je potrebno zato, da ji lahko sledi vprašaj: tisto, kar je v njej (torej v najbolj zunanjih oklepajih), se lahko pojavi ali pa tudi ne, odvisno od tega, ali si Bratko ali Colnar. Skupino, ki nas v resnici zanima, in vsebuje bodisi telefonsko bodisi URL, pa smo tudi poimenovali.

Funkcija, ki prebere imenik je tako

def preberi_imenik(): import re, urllib.request re_oseba = re.compile(r'<tr>.*?<div>(<a href="(?P<url>[^"]*)">)?\s*(?P<ime>[^<]*).*?<td class="lablinksPhone">'+ r'(<div><strong>T:</strong>\s*(?P<telefon>[^<]*)\s*</div>)?') stran = urllib.request.urlopen("http://www.fri.uni-lj.si/si/osebje").read().decode("utf-8") return [mo.group("ime", "telefon", "url") for mo in re_oseba.finditer(stran)]

Regularni izraz, ki smo ga napisali, je nekoliko tvegan, predvsem bi me skrbelo za .*?, pred katerim sem svaril tudi na forumu in v mailih - ta vam takoj zgrabi več kot bi bilo treba. Tule se mi zdi še kar varen, lahko pa bi se zgodilo, da stvar po kaki spremembi strani ne bi več delovala. A tako je v tem biznisu: vedno, ko boste pisali regularne izraze, s katerimi boste "kradli" vsebino spletnih strani, boste morali biti pripravljeni na to, da se bo stran nekoliko spremenila in boste morali izraze nekoliko popraviti, ali pa se bo spremenila popolnoma in bo treba pisati od začetka.

Last modified: Monday, 15 January 2024, 5:35 PM