3. Regularni izrazi

Regularni izrazi (Regular expression) su sastavni dio svakog naprednog alata za obradu teksta te svakog visokorazinskog programskog jezika. Oni su zapravo niz znakova koji predstavljaju obrazac koji se najčešće koristi za pronalaženje svih nizova znakova koji odgovaraju tom obrascu ili pak zamjenu tih nizova znakova. Primjer regularnog izraza koji opisuje većinu elektroničkih adresa je ovaj: r'[\w.-]+@(?:\w+[.])+\w+'. Taj obrazac opisuje niz znakova koji se sastoji od niza alfanumeričkih znakova koji mogu sadržavati i crticu, znaka @ te niza alfanumeričkih znakova i crtice koji završavaju točkom, ta se sekvenca može pojavljivati više puta, te konačno alfanumerički niz znakova. Iz ovog je primjera vidljivo da smo kroz nekoliko znakova definirali obrazac koji se riječima ne može tako kratko opisati.

Regularni izrazi imaju izražajnost regularnih jezika koji se pak mogu definirati regularnim gramatikama, najjednostavnijim gramatikama u Chomskyjevoj hijerarhiji formalnih jezika i gramatika (Chomsky hierarchy).

U Pythonu se podrška za regularne izraze nalazi u modulu re te ćemo ovdje koristiti redovito funkciju findall(pattern, string[, flags]) koja za neki uzorak, niz znakova te moguće zastavice vraća listu nizova znakova koji odgovaraju našem uzorku, odnosno regularnom izrazu.

Krenimo od početka s upoznavanjem znakova koji nose posebno značenje.

To su za početak znak točke . koja predstavlja bilo koji znak osim znaka prelaska u novi red, uglate zagrade [] koje omogućuju definiranje skupa znakova te znakova ^ i $ koji predstavljaju oznake početka i kraja niza znakova.

U sljedećem primjeru tražimo svako pojavljivanje dva znaka, od kojeg je prvi znak a, a drugi bilo koji znak osim prelaska u novi red u zadanom nizu znakova anafora:


In [30]:
import re
print re.findall(r'a.','anafora')


['an', 'af']

Module je potrebno jednom učitati naredbom import, nakon kojeg slijedi naziv modula (re). Nakon toga moguće je u programu pozivati funkcije i metode iz učitanog modula.

Na sljedeći način možemo u nizu anafora pronaći svako pojavljivanje dva znaka, od kojeg prvi znak mora biti jedan od samoglasnika (a ili e ili i ili o ili u), a drugi znak bilo koji znak osim prelaska u novi red:


In [31]:
print re.findall(r'[aeiou].','anafora')


['an', 'af', 'or']

U sljedećem primjeru tražimo bilo koja dva znaka na početku niza znakova:


In [32]:
re.findall(r'^..','anafora')


Out[32]:
['an']

Na sljedeći način tražimo bilo koja tri znaka koji se nalaze na kraju niza znakova:


In [33]:
re.findall(r'...$','anafora')


Out[33]:
['ora']

Druga važna skupina posebnih znakova je ona koja predstavlja kvantifikatore. Kvantifikatori su simboli koji označavaju koliko puta se neki znak, skup ili grupa znakova može, odnosno mora pojaviti. Razlikujemo četiri kvantifikatora:

  1. + kvantifikator odgovara jednoj ili više pojava
  2. * kvantifikator odgovara nula, jednoj ili više pojava
  3. ? kvantifikator odgovara nula ili jednoj pojavi
  4. {m,n} kvantifikator odgovara od m do n pojava

U sljedećem primjeru tražimo jedan ili više (+) samoglasnika ([aeiou]), odnosno najdulju sekvencu samoglasnika u nizu znakova (neaktivan):


In [34]:
print re.findall(r'[aeiou]+','neaktivan')


['ea', 'i', 'a']

Na sljedeći način tražimo bilo koji znak (.) iza kojeg slijedi nula, jedan ili više (*) samoglasnika ([aeiou]):


In [35]:
print re.findall(r'.[aeiou]*','neaktivan')


['nea', 'k', 'ti', 'va', 'n']

U sljedećem primjeru tražimo pojavu znaka e kojemu može, a ne mora slijediti znak a.


In [36]:
print re.findall(r'ea?','neaktivan')


['ea']

Na sljedeći način tražimo sekvencu samoglasnika duljine 2 do 5.


In [37]:
re.findall(r'[aeiou]{2,5}','neaktivaaaan')


Out[37]:
['ea', 'aaaa']

U Pythonovoj implementaciji regularnih izraza postoji i niz predefiniranih skupova znakova:

  • \d predstavlja znamenku, odnosno [0-9] dok \D predstavlja komplement prethodnog skupa, odnosno [^0-9] (znak ^ na početku popisa znakova predstavlja negaciju, odnosno komplement)
  • \s predstavlja skup svih praznina dok je \S komplement tog skupa
  • \w predstavlja skup alfanumeričkih znakova dok je \W komplement tog skupa

Kao treći argument funkciji findall() moguće je proslijediti i jednu ili više zastavica. Razlikujemo tri osnovne:

  • re.DOTALL čini da točka predstavlja svaki znak, pa i prelazak u novi red
  • re.MULTILINE čini da znakovi ^ i $ predstavljaju početak, tj. kraj svakog retka
  • re.UNICODE čini da skup alfanumeričkih znakova sadrži sve Unicode alfanumeričke znakove.

U sljedećem primjeru tražimo dva znaka, od kojih je prvi slovo s, a drugi bilo koji znak (.). Dodavanjem trećeg argumenta re.DOTALL, točka . može predstavljati bilo koji znak, pa i prelazak u novi red:


In [38]:
re.findall(r's.','danas\nsutra',re.DOTALL)


Out[38]:
['s\n', 'su']

Ako ne dodamo re.DOTALL, rezultat će biti sljedeći:


In [39]:
re.findall(r's.','danas\nsutra')


Out[39]:
['su']

U sljedećem primjeru tražimo prvi znak na početku reda. Dodavanje trećeg argumenta re.MULTILINE omogućuje pronalaženje prvog znaka na početku svakog reda, a ne samo prvog:


In [40]:
re.findall(r'^.','danas\nsutra',re.MULTILINE)


Out[40]:
['d', 's']

Ako izostavimo treći argument, rezultat će biti sljedeći:


In [41]:
re.findall(r'^.','danas\nsutra')


Out[41]:
['d']

U sljedećem primjeru tražimo sve Unicode alfanumeričke znakove:


In [42]:
re.findall(r'\w+',u'Kuhamo čaj.',re.UNICODE)


Out[42]:
[u'Kuhamo', u'\u010daj']

Ako izostavimo treći argument, slova s dijakritičkim znakovima biti će izostavljeni, a rezultat će biti sljedeći:


In [43]:
re.findall(r'\w+',u'Kuhamo čaj.')


Out[43]:
[u'Kuhamo', u'aj']

Nama je posebno važna posljednja zastavica re.UNICODE jer nam omogućava da regularnim izrazom +\w+ identificiramo najdulji niz alfanumeričkih znakova što će nam ugrubo predstavljati postupak rastavljanja niza znakova na riječi koje često nazivamo i pojavnicama. Iz tog razloga taj postupak nazivamo opojavničenje (tokenization) (Tokenization (lexical analysis))).

U sljedećem primjeru možemo opojavničiti sadržaj datoteke tekst.txt. Prvo je potrebno datoteku pročitati i ispravno dekodirati te taj sadržaj pohraniti u varijablu tekst nad kojom ćemo pokrenuti upit. Zatim je potrebno pomoću regularnih izraza pronaći sve alfanumeričke znakove (\w+) te omogućiti da budu pronađeni svi Unicode alfanumerički znakovi (re.UNICODE).


In [45]:
tekst=open('tekst.txt').read().decode('utf8')
print re.findall(r'\w+',tekst,re.UNICODE)


[u'Prvi', u'red', u'Drugi', u'red', u'Tre\u0107i', u'red']

Zadatke možete naći ovdje: Zadaci