Regulaaravaldised
Igal nädalal ilmuvad repositooriumisse uue nädala ülesanded. Selle praktikumi ülesannete saamiseks ära unusta teha git pull
(sinine diagonaalne nooleke IDE ülaservas).
Nende küsimuste vastused tuleks kirjutada vastavasse muutujasse failis src/main/java/week2/intro/RegexExercise.java. Sellel on ka vastav test, millega saab ennast kontrollida.
Põhilised operaatorid
RE1: Valik ja täheklassid
Kasutame operaatorit |
, et leida kõik read, kus esinevad sõnad "kalad" või "jalad". Selleks võib muidugi kirjutada kalad|jalad
ja avaldise ühise teguri "alad" võib viia sulgude taha: (k|j)alad
. Valikud üksikute tähtede vahel võib kirjutada kandiliste sulude vahele, näiteks (a|b|c|d|x|y|z)
asemel võib kirjutada [abcdxyz]
ja isegi [a-dx-z]
. Neid nimetatakse täheklassideks (character class). Kasutades kandilistes sulgudes loetletud tähti, greppige kõik read, mis sisaldavad sõnad "kalad" või "jalad".
RE2: Punkt ja eeldefineeritud täheklassid
Eriline sümbolite klass on .
, mis sobitub suvalise sümboliga. Proovime nüüd sisendfailist välja greppida sellised read, mis sisaldavad kuskil viietähelist sõne, mille lõpus on tähed "alad", näiteks "jalad" ja "palad". Rohkem eeldefineeritud klasse on regex101 lehel all paremas nurgas reference → common tokens, eriti kasulikud on \w
, \d
ja \s
, aga ka \b
, mis sobitub sõnade piiridega. Nende abil võime proovida oma regexit täpsustada, et sobituks ainult viietäheliste sõnadega, s.t. ei sobituks näiteks lausega "Meie mäealad on head alad."
RE3: Epsilon ja küsimärk
Kirjutame nüüd regulaaravaldise, et välja filtreerida read, mis sisaldavad sõna "color" nii inglise kui ka ameerika kirjaviisil. Selleks võib näiteks kirjutada colo(u|)r
. Praktikas kasutatakse ε asemel lihtsalt tühjust. See on aga niivõrd tüüpiline konstruktsioon, et selle jaoks on olemas sünonüüm: (E|)
asemel võib kirjutada E?
. Otsige nüüd üles read, mis sisaldavad sõnad "color" ja "colour", kasutades õiges kohas küsimärki.
Java ja regex101.com jaoks ei ole epsilon eritähendusega. Seega sobitub regex "colo(u|ε)r" sõnadega "colour" ja "coloεr". Epsilon on siin lihtsalt kreeka täht. Teoorias kasutatakse tühja sõne jaoks kas epsiloni või lambdat, aga praktikas on seda harva näha, kus sõnede tähistamiseks kasutatakse jutumärke ja tühja sõne jaoks ei olegi eraldi tähist vaja, vaid kirjutame lihtsalt ""
.
RE4: Kordamisoperaator Kleene'i tärn
Kordamise operaator *
võimaldab leida suvalise arvu tähekordusi, näiteks ja*
leiab üles kõik "jaa", "jaaa", ja "jaaaaa", aga kahjuks ka lihtsalt "j" ning "ja". Õigem oleks kirjutada jaaa*
(selle jaoks on ka olemas sünonüüm ja{2,}
). Kuna me tahame ühte või enamat kordust kaunis sagedasti, siis on selle jaoks eraldi tähis +
, nt. a+
on sama, mis aa*
. Proovige kirjutada regulaaravaldis, mis leiab üles kõik Rootsipärased "jaha!" moodi ütlused, kus a-de vahele satub üks "h", näiteks "jahaa", "jaaaaha" ja "jaaaahaaaaaaaaaa".
Tehete prioriteet & süntaktiline suhkur
Mitme tehte koos kasutamisel tuleb teada tehete prioriteeti (või kasutada tehete grupeerimiseks sulge). Proovige katsetamise teel jõuda selgusele, kas avaldis b|a*
on samaväärne avaldisega (b|a)*
või avaldisega b|(a*)
.
Küllap on teil vaikselt tekkinud arusaam, et paljud lisakonstruktsioonid lisavad ainult mugavust, aga vabalt saaks ka ilma nendeta hakkama, näiteks on aa*
, a{1,}
ja a+
samaväärsed. Konstruktsioone programmeerimiskeeltes, mis keele võimsust ei muuda, aga teevad kirjutamist mugavamaks, nimetatakse süntaktiliseks suhkruks. Te peaksite oskama näiteks (ab){2,4}
välja kirjutada ainult loengus defineeritud operaatorite abil.
Terve rida versus alamsõna matchimine
Teoorias huvitab meid ainult küsimus kas sõne kuulub regulaarse keele hulka või mitte, aga praktikas, kui me näiteks otsime alamsõne ja asendame selle millegi muuga, tekib lisaküsimusi konteksti kohta. On olemas teatud erisümbolid, mis sobituvad ridade ja sõnade algul olevate nähtamatute (tühjade) sümbolitega: ^
on nähtamatu sümbol rea algul, $
: nähtamatu sümbol rea lõpul ja \b
tähistab nähtamatut sümbolit sõna algul ja lõpul. Paljudel vahenditel on lisaks ka \<
ja \>
sõna alguse ja lõpu jaoks eraldi.
RE5: Binaarsõned
Proovime leida kõik read, mis ei sisalda muud kui ühtesid ja nulle. Kui lihtsalt kirjutada [01]*
, siis saame kogu sisendi tagasi, sest tühi sõne sisaldub igas reas. Siin peab kasutama ^
ja $
.
RE6: Eelviimane täht on a
Väljastage kõik read, kus on ainult 'a' ja 'b', aga mille eelviimane täht on "a".
Grupid, tagasiviited, ja operatsioonid regexitega
Lõpuks vaatame ühte operaatorit, mis väga selgelt eristab praktikas kasutatavaid regulaaravaldisi meie formaalsest definitsioonist. Mõnikord kasutatakse sõna "regex", et eristada neid formaalse teooria regulaaravaldistest. Peamine selline operatsioon on tagasiviited (back reference).
RE7: Tagasiviited
Iga sulupaar moodustab nummerdatud grupi (numbered capturing group), millele võib tagasi viidata, näiteks (.)(.)\2\1
sobitub neljatäheliste palindroomidega nagu "ABBA". Proovige ennustada, milliste sisenditega sobituvad ^(ab|ba)(ab|ba)$
ja ^(ab|ba)\1$
. Oluline on siin aru saada, mille poolest need kaks avaldist teineteisest erinevad. Kirjutage tagasiviitega regex, mis sobitub neljatäheliste ridadega, mis tükelduvad kaheks võrdeks osaks, näiteks "abab", "aaaa", "haha", jne.
RE8: Replace
Gruppe saab ka kasutada, et leitud sõne asendada teisega, näiteks saab teha järgmise teisenduse: "Eesnimi Perekonnanimi" -> "Perekonnanimi, Eesnimi". (Loengus oli näide vastupidisest suunast.) Proovige see teisendus Javas kirja panna. Proovige kõigepealt, kas oskate lihtsalt ära tunda Eesnimi Perekonnanimi? Asendamisel saab gruppidele viidata dollariga, näiteks $1
. Abiks võivad olla ka eeldefineeritud tähetüübid, vt. Pattern klassi doc. Võite ülalolevat teisendust proovida teha ka oma lemmik tekstiredaktoris search&replace funktsionaalsuse abil. MS Wordi jaoks on juhend siin.
RE9: Lazy vs. greedy
Kui teksti otsimiseks ja asendamiseks kasutatakse regulaaravaldisi, siis tekib veel küsimusi, näiteks millega peaks "p.*p" sobituma sisendis "paapapaaaap"? Teoreetiliselt ei ole ju vahet, sest nii terve sõne, kui ka alamsõne "paaap" kuuluvad regulaaravaldise keelde. Teksti asendamisel on aga oluline aru saada "lazy versus greedy" kordamise operaatoritest. Proovige leida võimalikult lihtne regex, millega lauses "This is (not) important (for me)" eemaldada sulud, et tulemus oleks "This is important". Siin on probleem selles, et "\\(.*\\)"
sobitub terve alamsõnega "(not) important (for me)", aga tahaks, et sobituks sisemiste sulupaaridega.
Tehniline lisamärkus...
Siin tasub lõpuks mainida, et on üks oluline erinevus loengus esitatud meetodite ja paljude programmeerimiskeelte regex mootorite vahel. Lekseri ja Posixi töövahendid on tekstipõhised ning võtavad alati tekstist pikima sobituse, aga Java on regexipõhine ehk töötleb regexit vasakult paremale, näiteks:
-
"ab".replaceAll("a|ab", "x")
==>"xb"
-
"ab".replaceAll("ab|a", "x")
==>"x"
Seevastu on Posixi käsu sed -E 's/a|ab/x/' <<< "ab"
tulemuseks ikkagi "x". Posixi järgi on "a|ab" ja "ab|a" ja "ab?" kõik võrdsed, aga Javas on alternatsiooni järjekord oluline: kui esimene valik sobitub, siis teist enam ei proovita.