Regularni izrazi
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.
- 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.
- 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.
- 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!"
- (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.
- *?, +?, ??
- Č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.
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:
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
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
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.
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:
Metoda sub
je podobna metodi replace
, ki smo jo spoznali pri nizih: z njo lahko zamenjamo vse pojavitve danega vzorca s čim drugim.
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
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:
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.
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:
Š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:
[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
(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.
Gre pa, jasno, tudi hitreje: modul collections
ima razred Counter
, ki prešteje število ponovitev v poslanem seznamu, slovarju, množici... Takole:
To naš program precej poenostavi, saj nas odreši glavnega opravila, štetja.
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
, 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:
< | oklepaj, < |
\s* | poljubno praznega prostora (lahko tudi nič) |
img | beseda img |
[^>]* | poljubno dolga poljubna šara, razen zaključnega oklepaja |
\W | karkoli, kar ni črka |
src | beseda 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 |
src
ja 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* | |
img | img |
[^>]* | |
\W | (presledek med img in src ) |
src | src |
\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 ) |
img | img |
[^>]* | 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 ) |
src | src |
\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.
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 object
a 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 src
em. 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
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
. Funkcija s takšnim regularnim izrazom je taka:
Primer: branje telefonskega imenika FRI
Na strani s seznamom osebja FRI vsaki osebi ustreza vrstica podobna tej
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>
Regularni izraz, s katerim ulovimo opis osebe - krajši ali daljši -, je