» »

Perl RegEx

Sistemski administratorji, programerji in drugi zahtevnejši uporabniki se velikokrat srečujemo z zahtevo po obdelavi različnih kosov besedila takšnih in drugačnih oblik. Včasih gre za sezname, drugič za HTML strani, tretjič za velika besedila. Včasih želimo po naših besedilih kaj poiskati, drugič urediti in še največkrat nekaj spremeniti. Morda bo kdo mislil, da se ga takšni problemi sploh ne tičejo, saj ne potrebuje podobnih obdelav, a se velikokrat izkaže, da bi nam nekaj znanja na tem področju prišlo še kako prav, če bi ga le pravočasno pridobili.

Težava je v tem, da se zdi, da je iskanje po besedilih in njihova obdelava precej enostavna zadeva, a kmalu uvidimo, da precejkrat temu žal ni tako. Seveda večina klenih programerjev najprej pomisli, da po pascalovskem "string" ali Cjevskem "char *" tipu le ni tako težko iskati, a se ob kakšnem tršem orehu kaj hitro strezni.


Lotimo se primera

Vzemimo srednje težak primer. Recimo, da želite napisati grabežljivca elektronskih naslovov iz spletnih strani. Dobite goro html datotek, iz katerih morate potegniti vse nize, ki bi lahko predstavljali želene naslove. Želeli bi dobiti vse nize, ki vsebujejo črke A-Z, a-z, poleg tega pa številke 0-9 in podčrtaje. Vse skupaj mora biti sestavljeno tako, da vsebuje natanko en znak '@' in desno od njega vsaj eno piko, hkrati pa se niz ne sme končati s piko. Veliko sreče.

Naš odrešenik sliši na ime RegEx, kar je okrajšava za Regular expression. Če bi zadevščino poskušali prevesti v slovenščino bi se imenovala Regularni izraz, vendar bomo v nadaljevanju zaradi same praktičnosti izraza uporabljali kar okrajšavo regex.

RegEx ima dve osnovni rabi:

  • iskanje besedila
  • iskanje besedila in zamenjava z novim besedilom
Različni jeziki podpirajo RegEx na različne načine. Tako imajo PHP, Perl in večina ostalih skriptnih jezikov zadevščino že vgrajeno v sam jezik, medtem ko obstajajo za C++, Javo in druge jezike posebne knjižnice, ki omogočijo enake funkcije.

Za naše potrebe si bomo pogledali, kako deluje RegEx v jeziku Perl. Oboževalci drugih jezikov ne skrbite, saj je, na primer, v PHPju regex do potankosti enak tistemu iz Perla. V nadaljevanju članka se pričakuje osnovno poznavanje sintakse Perla. Ta pa je dovolj enostavna, da bo vsak, ki je v življenju že sprogramiral nekaj vrstic kode, z lahkoto razumel, kaj program počne. Preden pa se poskušamo postaviti na noge in opraviti prve korake v regex, naj povem le še, da je to tako obširna tema, da bi bile o njej lahko napisane cele knjige in začuda tudi so.


Kdor išče, ta najde

Na vhodu programa najdimo vse pojavitve besede "Slo-tech". Stvari se bomo lotili po vrsticah, brali bomo torej vrstice eno za drugo, v njih pa iskali niz "Slo-tech".

01 $i=0;
02 while ( $vrstica = <> ) {
03	$i = $i + 1;
04	if ( $vrstica =~ m/Slo-tech/ ) {
05		print "Vrstica " . $i . " Vsebuje niz 'Slo-tech'\n";
06	}
07 }

Razčlenimo zgornji program. V prvi vrstici nastavimo števec $i (v perlu se z predpono $ označuje spremenljivke) na 0. To sicer naredi perl tudi samodejno, a tokrat smo zaradi jasnosti to eksplicitno napisali. V zanki med vrsticama 2 in 7 se iz standardnega vhoda bere posamezne vrstice, dokler so na voljo, in se jih shranjuje v spremeljivko $vrstica. V tretji vrstici za vsako vrstico povečamo števec $i -- tako vedno vemo, v kateri vrstici se nahajamo.

Naš regex na sceno nastopi v četrti vrstici. Kadar nad spremenljivko uporabimo operator =~, pomeni da bo sledil regex. Regex za iskanje besedila se začne s črko m (angleško "Match", nakazuje ujemanje), sledi mu poševnica /, iskan niz, in še ena poševnica. Pogojni stavek je resničen, kadar $vrstica vsebuje niz "Slo-tech". Ostane nam le še vrstica 5, v kateri preprosto izpišemo številko vrstice, v kateri smo našli niz.


Kdor z malimi zadovoljen ni, velikega vreden ni

Zgornji primer je občutljiv na velikost črk. To pomeni, da naš programček niza "Slo-TeCH" ne bo zaznal. Da naj regex ne upošteva velikosti črk, mu povemo s parametrom "i" (Ignore case), ki ga dodamo na koncu regexa. Če želimo iskanje neobčutljivo na velikost črk, mora 4. vrstica izgledati takole:

04	if ( $vrstica =~ m/slo-tech/i ) {

"Slo-tech" smo tu črkovali z malimi črkami, a ker stoji na koncu /i, je popolnoma vseeno, ali uporabimo velike ali male inačice črk.


Slovenija, dežela mog sna

Zdaj pa bi radi poiskali vse besede, ki se začenjajo na "slo-", nadaljujejo pa s poljubnimi črkami abecede. Četrta vrstica bi izgledala takole:

04	if ( $vrstica =~ m/slo-[a-z]*/i ) {

Kadar uporabimo oglate oklepaje, vemo, da bomo z njimi določili en sam znak, ki je lahko katerakoli izmed navedenih črk. Pri tem lahko uporabimo tudi znak '-' za navajanje intervala. Namesto izraza [a-z] bi lahko napisali tudi [abcdefgijklmnopqrstuvwxyz]. Seveda pa smo opazili še en nov znak, to je zvezdica. Ta znak pove, da se znak, ki stoji pred njim (v našem primeru znak, ki ustreza [a-z], kar pomeni katerokoli črki), ponovi poljubno mnogokrat. Torej, za nizom "Slo-" naj stoji še poljubno mnogo črk. Vendar pa zvezdica lahko pomeni tudi, da niz, ki bo ustrezal, ne bo vseboval nobenega znaka sploh. Če želimo zagotoviti, da bo za nizom "Slo-" stala vsaj še ena črka, bomo namesto zvezdice uporabili znak plus: "+".

Če torej želimo najti le nize, ki poleg niza "Slo-" vsebujejo vsaj še eno črko, bomo namesto zvezdice uporabili plus in dobili:

04	if ( $vrstica =~ m/slo-[a-z]+/i ) {

Obstaja pa še možnost, da Perlu bolj kompaktno povemo, kaj bi radi. Tako lahko [a-z], ki predstavlja poljubno črko abecede, nadomestimo z posebnim nizom \w, ki predstavlja katerokoli črko abecede ali katerikoli številko. Bolj natančno povedano, je to ekvivalenca izrazu [a-zA-Z0-9_]. Naš novi, bolje delujoč primer, bo torej izgledal takole:

04	if ( $vrstica =~ m/slo-[\w]+/i ) {

Resnica, samo resnica in nič drugega kot resnica

Sedaj pa se pojavi težava. Ko bo naš program v tretji vrstici vhoda našel niz 'slo-mach3' bo vseeno izpisal "Vrstica 3 Vsebuje niz 'slo-tech'". To seveda ni res, saj vrstica vsebuje niz "slo-mach3" in ne "slo-tech". Seveda želimo, da bi program izpisal niz, ki ga je dejansko našel. To storimo z uporabo navadnih oklepajev. Vse, kar bo v navadnih oklepajih, bo perl shranil v posebne spremenljivke z imeni $1, $2, $3, itd. Ime spremenljivke predstavlja zaporedno številko oklepajnega para, znotraj katerega je bil najden niz. Če želimo torej izpisati, kaj točno je regex našel, bomo spremenili 4. in 5. vrstico.

04	if ( $vrstica =~ m/(Slo-[\w]+)/i ) {
05		print "Vrstica " . $i . " Vsebuje niz " . $1 . "\n";

Na svetu so tri vrste ljudi. Tisti ki znajo in tisti ki ne znajo šteti.

S tem pa težavam, ki smo jih ustvarili z možnostjo, da bomo našli več različnih nizov, ki bodo ustrezali našim pogojem, še ni konca. Vzemimo za primer naslednjo vrstico: "Slo-tech je najboljši, ampak tudi Slo-Mach in Slo-Owca nista od muh". Naš program bo našel le prvi niz "Slo-tech", ga izpisal in se odpravil v naslednjo vrstico, druga dva sicer ustrezajoča niza pa bo prezrl. Seveda si želimo, da bi našel vse nize, ki ustrezajo našem regexu. Rešitev izgleda takole:

04	while ( $vrstica =~ m/(Slo-[\w]+)/ig ) {
05		print "Vrstica " . $i . " Vsebuje niz " . $1 . "\n";

Kaj smo naredili? Najprej smo v četrti vrstici stavek "if" zamenjali s stavkom "while". Razlika je v tem, da je if le enkrat preveril, če je bil pogoj izpolnjen, medtem ko while preverja pogoj vse dotlej, dokler ni več izpolnjen (torej dokler niz ni več najden). A to bi se brez druge spremembe izkazalo za neskončno zanko. Vsako iskanje bi se začelo na začetku stavka, zato bi niz vedno našli. Posledično dejstvu moramo dodati regexu na koncu kot parameter še znak "g". Ta regexu pove, naj iskanje nadaljuje od tam, kjer je ostal prejšnjič. Če bo tako prvič v zgornjem stavku našel niz "Slo-tech", se bo naslednje iskanje začelo pri presledku, ki mu sledi, in bo naslednji najden niz "slo-Mach", nato "slo-Owca", nato pa iskani niz ne bo več najden in zanka while se bo ustavila, izvajanje programa pa se bo nadaljevalo s preverjanjem naslednje vrstice.

Sedaj bo izpis programa ob vhodni vrstici zgoraj pravilen. Izpisal bo vse tri nize ("Slo-tech", "slo-Mach" in "slo-Owca"). Velja omeniti še, kako se regex obnaša, če en niz lahko vsebuje drugega. Tako bi laho na primer imeli vrstico, ki bi vsebovala niz "slo-slo-tech". Vprašanje, ki si ga zastavimo, je seveda kaj bo izpisal naš program. Program bo najprej našel besedo "slo-slo", ki jo bo tudi izpisal. Iskanje bo v tem trenutku pri drugi črtici, zato bo v nadaljnem iskanju moč pregledati le še niz "-tech", ki očitno ne ustreza našim pogojem. Edini zadetek bo torej "slo-slo".

To pa še ni vse. Recimo, da želimo iskati niz "aba*" (torej niz "ab", ki mu sledi še poljubno mnogo znakov "a"). Če ga iščemo v nizu "abaaaaba" bo edini zadetek predstavljal "abaaaa", saj bo zvezdica poskušala zajeti kar čimveč ajev, kar bo pripeljalo, da bo zajela vse srednje štiri. Preostanek niza bo le še "ba", ki očitno ne bo ustrezal našim iskalnim pogojem. Požrešno obnašanje zvezdice lahko spremenimo tako, da ji na koncu dodamo še vprašaj. Če bi v nizu "abaaaaba" iskali z regexom "aba*?" bi dobili dva zadetka, saj bi zvezdica namesto požrešno delovala minimalistično. Dva zadetka bi dobili tudi če bi iskali v nizu "abab", saj zvezdica pomeni, da znak, ki je pred njo, ni nujno prisoten.


Več kot je znakov, bolj je veselo

Ostane nam še vprašanje, kako opisati ponavljanja več zaporednih znakov. To lahko storimo z uporabo navadnih oklepajev (to je poleg definiranja začasnih spremenljivk njihova druga funkcija). Tako lahko recimo poiščemo vse nize, ki imajo na začetku enkrat ali večkrat niz "slo", ki mu sledi črtica, za njo pa katere koli črke:

04	while ( $vrstica =~ m/((Slo)+-[\w]+)/ig ) {
05		print "Vrstica " . $i . " Vsebuje niz " . $1 . "\n";

Ta regularni izraz se bo ujemal nizom "slosloSLO-n", medtem, ko se z "-tech" ali "slo-" ne bo.


Pobeg iz Absoloma

Pojavi pa se še eno vprašanje: kako opisati znake, ki imajo nek poseben dodaten pomen? Doslej smo spoznali pomen znakov *, +, (, ) in \. Rešitev je preprosta -- uporabimo tako imenovano ubežno sekvenco -- pred poseben znak enostavno postavimo poševnico. Če bi želeli iskati niz "slo+tech", bi to zapisali takole:

04	while ( $vrstica =~ m/slo\+tech/ig ) {
05		print "Vrstica " . $i . " Vsebuje niz 'slo+tech'\n";

Clark Kent -> Superman?

Sedaj pa si poglejmo, kako lahko z uporabo regularnih izrazov nize zamenjujemo. Tokrat bomo datoteko na vhodu izpisali na izhod, med tem pa izvedli še nekaj zamenjav nizov. Najprej bomo zamenjali vse pojavitve niza 'Slo-tech' z nizom 'Bloat-tech'.

01 while ( $vrstica = <> ) {
02	$vrstica =~ s/Slo-tech/Bloat-tech/g;
03	print $vrstica;
04 }

Struktura programa je podobna prejšnjemu. V prvi vrstici se vrtimo v zanki in shranjujemo v spremenljivko $vrstica vsako vrstico vhoda. V vrstici 2 opravimo zamenjavo s pomočjo regexa, v tretji vrstici pa spremenjeno $vrstica izpišemo na standardni izhod. Podrobneje nas zanima vrstica številka 2.

Zopet vidimo, da bomo uporabili operator =~ vendar ga tu ne bomo uporabili za preverjanje pogoja, temveč bo že sam spremenil niz. Regex za iskanja se začne s črko s (angleško "Search and replace", torej "najdi in zamenjaj"). Nadaljuje se s poševnico, iskanim nizom ter še eno poševnico. Za drugo poševnico navedemo niz, s katerim naj se iskani zamenja, na koncu pa sledi še ena poševnica. Prav tako kot pri ukazu za ujemanja m, veljajo tudi za s enaki končni parametri. Tako bi bil brez črke g zamenjan le prvi najden niz v vrstici. Lahko pa bi uporabili tudi črko i, ki bi povzročila, da iskanje ne bi bilo občutljivo na velikost črk.


Kar se Janezek nauči, to Janez zna

Kaj pa, če želimo zamenjati vse pojavitve "slo-"KARKOLI s "slo-mach"? Nič lažjega, uporabimo enake trike kot smo se jih naučili na primerih 1-7. Druga vrstica bi se tako glasila:

02	$vrstica =~ s/Slo-\w+/slo-mach/gi;

Tako bomo našli vse nize, ki se začenjajo na 'slo-' in nadaljujejo z vsaj eno črko in jih zamenjali v niz 'slo-mach'. Ukazu smo na koncu dodali še parameter i, ki povzroči neobčutljivost na velikost črk.


Zalimbajmo skupaj

Seveda pa si želimo z nizi pri zamenjavah početi tudi pohujšljivejše vragolije. Recimo, da želimo obrniti vrstni red besed. Poiskali bomo pojavljanja besed 'slo-'NEKAJ in jih spremenili v NEKAJ'-slo'. Za to bomo uporabili začasne spremenljivke, ki pa se jih znotraj regexa ne nasljavlja zaporedoma kot $1, $2, $3 temveč kot \1, \2, \3, itd.

02	$vrstica =~ s/slo-(\w+)/\1-slo/gi;

Tiste črke, ki bodo stale za nizom 'slo-' smo dali v oklepaje, kar pomeni, da se bodo spravile v prvo zaporedno spremenljivko (torej 1). Ob zamenjavi pa bomo spremenljivko 1 uporabili na drugem mestu in s tem zamenjali vrstni red. Vendar pa bo zgornji program našel niz "SLO-TECH" (zaradi parametra i na koncu) ter ga zamenjal v "TECH-slo". Želeli bi si, da bi drugi del besede po velikosti črk ustrezal originalu:

02	$vrstica =~ s/(slo)-(\w+)/\2-\1/gi;

To smo storili z uporabo dveh začasnih spremenljivk. Prva je tista, kateri ustreza niz "slo" ne glede na velikost črk, druga pa vsebuje pozitivno število črk. V drugem delu regexa jih le sestavimo v obratnem vrstnem redu.


You can hide, but you can't run away

Isto spremenljivko lahko uporabimo celo znotraj istega dela regexa, tako lahko iščemo ponavljajoči se besedi, med katerima je znak "-" in ga zamenjamo s črko "A".

02	$vrstica =~ s/(\w+)-(\1)/\1A\1)/g;

Regularni izrazi v splošnem delujejo zadovoljivo, če ne že fantastično hitro. A kot vedno tudi tu obstajajo izjeme. Za tiste bolj teoretično razgledane naj povem, da ravno omenjena lastnost uporabe začasnih spremenljivk (oziroma back references) znotraj istega izraza povzroči, da je mogoče s Perlom sestaviti izraz, katerega ustrezanje oziroma matching je NP-poln problem. To pomeni, da čas potreben za izvajanje raste eksponentno glede na količino besedila, po katerem se išče. Zabavno, ne?


In na koncu pika

Na koncu pride seveda še pika. Pika je eden od bolj uporabnih posebnih simbolov regexa, njen pomen pa je popolnoma enostaven: ustreza kateremu koli znaku. Tako lahko, recimo, poiščemo vse nize, ki ustrezajo obliki 'slo'NEK_ZNAK'tech', ter ta vmesni znak spremenimo v niz "-je zakon-".

02	$vrstica =~ s/(slo).(tech)/\1-je zakon-\2/gi;

Tako bodo vsi nizi kot so "slo tech", "slo-tech", "slootech", "slo4tech" zamenjani v "slo-je-zakon-tech". Hkrati pa bodo nizi "slo---tech" ali "sloooooteh" ostali nespremenjeni. Če bi želeli, da je med besedama "slo" in "tech" poljubno mnogo znakov, dodamo še zvezdico.

02	$vrstica =~ s/(slo).*(tech)/\1-je zakon-\2/gi;

Odlično. Vendar pazite, če boste skozi slednji regex poslali niz "Slovenija je prelepa dežela. Še posebno, ker imamo Slo-Tech", bo iz vsega skupaj nastalo le "Slo-je zakon-tech".


Ne začetek konca, temveč konec začetka

Čeprav se zdi regex zapletena zadeva je, ko se ga enkrat navadimo, neprecenljiv. Vsebuje na stotine bolj ali manj uporabnih možnosti, ki si jih nikoli ne bomo zapomnili. Dobro je le vedeti, da obstjajo na spletu odlične strani, ki jih podrobno opisujejo.

Šifriranje nosilcev podatkov v okolju Linux in Windows

Šifriranje nosilcev podatkov v okolju Linux in Windows

Cryptography is a data-protection technology just as gloves are a hand-protection technology. Cryptography protects data from hackers, corporate spies and con artists, whereas gloves protect hands from cuts, scrapes, heat, cold and infection. The former can frustrate FBI wiretapping, and the latter can ...

Preberi cel članek »

Varnost v PHPju

Varnost v PHPju

Dandanes ogromno ljudi uporablja PHP z namenom, da hitro sestavijo svoje dimanično generirane spletne strani. Ob tem seveda nihče ne pomisli na varnost oziroma na pisanje &quot;varne&quot; kode - brez oziroma z malo možnosti vdora in zlorabe strežnika, na katerem so skripte postavljene. ...

Preberi cel članek »

Sed

Sed

Velika količina podatkov je in še dolgo bo v tekstovni obliki. HTML, XML, /etc/passwd in večina drugih konfiguracijskih datotek, celotna linux dokumentacija, PDF in DOC (bolj ali manj), mnoge knjige in sodbe sodišč, dolgočasni samoupravni sporazumi. V tem in še enem ...

Preberi cel članek »