Groovy a wyrażenia regularne
Wprowadzenie
Teraz gdy już przebrnąłem przez podstawowe metody WE/WY zacząłem kombinować jak wykorzystać Grooviego do przetwarzania plików tekstowych. A wiadomo, że gdy mamy na myśli język skryptowy i ocenę jego przydatności we wspomnianym zakresie to koniecznie należy przyjrzeć się w jaki sposób obsługuje on wyrażenia regularne.
Wydaje mi się że o mocy która kryje się w wyrażeniach regularnych nikogo przekonywać nie trzeba. Naprawdę nie mogę zrozumieć, dlaczego tak mało edytorów tekstu tworzonych pod okna wspiera i wykorzystuje ten potężny mechanizm wyszukiwania i przetwarzania wzorców. Ale do rzeczy.
Do przetwarzania wyrażeń regularnych Groovy wykorzystuje biblioteki dostarczane z JDK. Oczywiście wprowadza szereg udogodnień, które sprawiają, że przeszukiwanie tekstów pod kątem występowania w nich jakiegoś wzorca jest dużo prostsze i przyjemniejsze niż w samej Javie.
Dzięki mechanizmom wyrażeń regularnych Groovy pozwala na:
- określenie czy (a jeśli tak to ile razy) w tekście wystąpił ciąg znaków zgodny ze wzorcem
- wykonanie określonych operacji na dopasowanym łańcuchu znaków
- dokonanie zastąpień dopasowanych łańcuchów znakowych
- podział dopasowanego ciągu na podciągi
Definiowanie wzorców
Przetwarzanie tekstów z wykorzystaniem wyrażeń regularnych wiąże się z koniecznością definiowania wzorców dopasowań. Są to instrukcje dla mechanizmu przetwarzającego zawierające informacje czego ma szukać i w jaki sposób to przetwarzać. Mają one postać ciągów znakowych, zawierających pewne symbole o specjalnym znaczeniu. Dzięki nim możemy określić pewne grupy znaków, liczb, które mają zostać wyszukane w tekście. Najważniejsze z nich to:
- . (kropka) - dowolny znak
- ^ - początek linii
- $- koniec linii
- \d- dowolna liczba
- \D- dowolny znak nie będący liczbą
- \s- biały znak
- \S- dowolny znak nie będący znakiem białym
- \w- słowo
- \W-znaki nie układające się w słowa
- \b-granica słowa
- ()-operator grupowania
- (a|b)-warunek logiczny "lub" (a lub b)
- * - zero lub więcej wystąpień poprzedzającego znaku(grupy)
- + - jeden lub więcej wystąpień poprzedzającego znaku(grupy)
- ? - dokładnie jedno wystąpienie poprzedzającego znaku(grupy)
- x{m,n} - co najmniej m i co najwyżej n znaku(grupy) poprzedzajacej
- x{m} - dokladnie m wystapien znaku (grupy) poprzedzajacej
- /\d\d-\d\d-\d\d\s?\d\d:\d\d/ lub w uproszczeniu \d{2}-\d{2}-\d{2}\s?\d{2}:\d{2} - dopasuje się do wszystkich dat zapisanych w formacie "liczba liczba-liczba liczba- liczba liczba SPACJA liczba liczba:liczba liczba", czyli np. 01-02-07 10:22
- /href=".*?"/ - maska pozwalająca na wyszukanie w tekście wystąpień adresów URL
Dopasowanie zachłanne
Operatory rozszerzające (takie jak * i +) mają pewną cechę, o której zawsze należy pamiętać. Chodzi o to, że dążą one do dopasowania jak najszerszego możliwego ciągu znakowego pasującego do zadanego wzorca.Spójrzmy na następującą maskę: /href=".*"/.
Należy rozumieć ją następująco:
dopasuj się do ciągu znaków, rozpoczynającego się słówkiem "href", po którym występuje znak "=" i cudzysłów ("), w jego wnętrzu ma znaleźć się zero lub więcej dowolnych znaków kończących się cudzysłowem.
Wydawać by się mogło, że taki wzorzec prawidłowo wyciągnie nam wszystkie adresy stron w postaci href="adres". Tak rzeczywiście by było gdyby nie zachłanność dopasowania operatora *.
Problem występuje w sytuacji gdy wyrażenie regularne zostanie zastosowane np. do następującego tekstu:
To jest (href="www.pierwszy.adres.url") pierwszy rozdzial (href="www.drugi.adres.url")
Dopasowanie nastąpi w następujący sposób: znaleziony zostanie następujący ciąg znaków: href=", i jak dotąd wszystko jest zgodne z oczekiwaniami, jednak potem zaczynają się problemy, bo każdy znak, który pojawi się potem, aż do napotkania OSTATNIEGO cudzysłowu zostanie uznane jako pasujące do ".*", a więc wynikiem zastosowania reguły wzorca będzie ciąg znaków:
href="www.pw.edu.pl") pierwszy rozdzial (href="www.bla.bla.pl"
Czy o to mi chodziło ? Niekoniecznie....
SPOSÓB OGRANICZENIA ZACHŁANNOŚCI:
Aby ograniczyć zachłanność operatora, należy we wnętrzu reguły, bezpośrednio po znaku "*", a przed znakiem ograniczającym zakres dopasowania (u nas cudzysłów) znaku "?". Spowoduje to dopasowanie się nie do maksymalnie najdłuższego łańcucha, tylko do najkrótszego.
A więc nasza reguła wybierająca powinna wyglądać następująco: /href=".*?"/. Wynik jej działania jest już zgodny z naszymi oczekiwaniami :
href="www.pierwszy.adres.url"
href="www.drugi.adres.url"
Wykorzystywanie wyrażeń regularnych
Metody find() i matches()
Wyrażenia regularne w Groovym obsługiwane są przez klasę java.util.regex.Matcher, która udostępnia dwie bardzo użyteczne metody- find() i matches().
- matcher.find()- pozwala na określenie czy w określonym ciągu znakowym, da się wyszukać podciąg zgodny ze wzorcem dopasowania
- matcher.matches()- przeszukuje łańcuch wejściowy i próbuje dopasować cały wzorzec
- =~ jest tożsamy z uruchomieniem metody matcher.find()
- ==~ - jest tożsamy z uruchomieniem metody matcher.matches()
text='1 2 4 8 64'
(text=~/4/).each{match->print match}
(text=~/4/) - zwraca obiekt matcher, który zawiera w sobie dopasowane ciągi znaków ( w tym przypadku wystąpienia znaku 4)
Metoda String.eachMatch(regex)Alternatywnym i w moim przekonaniu bardziej intuicyjnym sposobem dotarcia do obiektów otrzymanych w wyniku zastosowania wzorca, jest metoda String.eachMatch(wyrazenie_regularne).
Dzięki niej i wykorzystaniu mechanizmu domknięć możliwe jest przetworzenie każdego podzbioru znaków, który na podstawie wzorca wyrazenie_regularne zostani wyodrębniony z obiektu String, na którym wywoływana jest metoda. Np.
pattURL=/href=".*?"/
content=new File("regularExp_source.txt").getText();
content.eachMatch(pattURL){ match->
print "\n"+match[0]
}
Powyższy skrypt powoduje że w pliku regularExp_source.txt wyszukane zostaną wszystkie adresy url. Należy zwrócić uwagę na ciekawą rzecz. Mianowicie obiekt matcher wyniki dopasowania zwraca nie w postaci pojedynczego łańcucha znaków, lecz tablicy, pod której zerowym indeksem znajduje się wynik dopasowania. Jeśli we wzorcu wystąpiło n operatorów grupowania () to każda z dopasowanych grup znajdzie swe miejsce pod kolejnym indeksem.
Metoda String.replaceAll(regex)
Inną ciekawą metodą, jest String.replaceAll(regex). Pozwala ona na zamianę jednych ciągów znakowych na inne.
Schemat jej działania jest następujący:
content.replaceAll(/wzorzec/){it - 'ciągZastępowany'+'ciągZastępujący'}. Aby zamiana nastąpiła ciągZastępowany musi zostać dopasowany przez wzorzec.
Przykładem zastosowania jest skrypt zamieniający polskie znaki na ich liczbowe reprezentacje w utf.
myFile=new File("znakiUTF.txt") myFile.write("\n")
content=new File("znakiAscii.txt").text
content=content.replaceAll(/Ą/){it - 'Ą'+'\\\\u0104'}
content=content.replaceAll(/ą/){it - 'ą'+'\\\\u0105'}
content=content.replaceAll(/Ć/){it - 'Ć'+'\\\\u0106'}
content=content.replaceAll(/ć/){it - 'ć'+'\\\\u0107'}
content=content.replaceAll(/Ę/){it - 'Ę'+'\\\\u0118'}
content=content.replaceAll(/ę/){it - 'ę'+'\\\\u0119'}
content=content.replaceAll(/Ł/){it - 'Ł'+'\\\\u0141'}
content=content.replaceAll(/ł/){it - 'ł'+'\\\\u0142'}
content=content.replaceAll(/Ó/){it - 'Ó'+'\\\\u00D3'}
content=content.replaceAll(/ó/){it - 'ó'+'\\\\u00F3'}
content=content.replaceAll(/Ś/){it - 'Ś'+'\\\\u015A'}
content=content.replaceAll(/ś/){it - 'ś'+'\\\\u015B'}
content=content.replaceAll(/Ź/){it - 'Ź'+'\\\\u0179'}
content=content.replaceAll(/ź/){it - 'ź'+'\\\\u017A'}
content=content.replaceAll(/Ż/){it - 'Ż'+'\\\\u017B'}
content=content.replaceAll(/ż/){it - 'ż'+'\\\\u017C'}
myFile.write(content,"UTF-8")