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
Wzorce dopasowań, rozpoznawane przez Grooviego ograniczane są znakami "/"(slash). Przykładowe definicje masek wyglądają następująco:
  • /\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
Pod adresem http://www.nvcc.edu/home/drodgers/ceu/resources/test_regexp.asp znaleźć można proste narzędzie, które służy do testowania wrażeń regularnych.

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
Dla uproszczenia Groovy definiuje dwa pomocne operatory:
  • =~ jest tożsamy z uruchomieniem metody matcher.find()
  • ==~ - jest tożsamy z uruchomieniem metody matcher.matches()
Metoda find() zwraca obiekt matcher. Aby dostać się do wszystkich zwróconych przez nią obiektów można wywołać jej metodę each() - wraz z zastosowaniem domknięcia. Przykładem działania niech będzie skrypt wyszukujący w łańcuchu znaków wystąpień cyfry 4.

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")