Wyrażenia regularne w Javie – figle i psikusy

namiekko.pl 7 lat temu

To trzeci (i na jakiś czas ostatni) z serii wpisów na temat wyrażeń regularnych w Javie, po O wyrażeniach regularnych. Podstępna różnica pomiędzy find i matches oraz Wyrażenia regularne dla nieprogramistów. Historycznie ten jest najwcześniejszy – to zaktualizowana wersja tekstu z mojego poprzedniego bloga.

Poruszam tu bardzo podstawowe kwestie, które potrafią jednak dać się we znaki. Rozwiązanie tych paru niewinnych problemików kosztowało mnie niemało czasu i nerwów. Liczę, iż kiedy jakaś zbłąkana dusza znajdzie się w tej samej sytuacji, Google zaprowadzi ją prosto w moje troskliwe ramiona.

Artykuł powstał podczas mojej pracy nad narzędziem do przekształcania metadanych.

Psikus 1: znaki ucieczki w wyrażeniach wczytywanych z zewnętrznego pliku

Załóżmy, iż wyrażenie (które chcemy wczytać z zewnętrznego pliku) w zwykłym kodzie Javy wygląda tak:

Pattern yearPattern = Pattern.compile("^\\d{2}[-/]\\d{2}\\s*w\\.$");

Linia przedstawia zakres wieków, do którego dopasuje się na przykład linia:

14-16 w.

Proste, prawda?

Prawda – ale do czasu. Problemy pojawiły się, kiedy zaczęłam wczytywać wyrażenia regularne zapisane w zewnętrznym pliku. Wczytywałam między innymi następujący fragment:

^\\d{2}[-/]\\d{2}\\s*w\\.$

Wszystko przestało działać. Łańcuchy znaków, które bez najmniejszych wątpliwości powinny były dopasować się do wyrażenia, przechodziły niezauważone. Po godzinie analiz niebezpiecznie zbliżałam się do stanu, w którym myślałam, iż oszalał albo świat dokoła mnie. Wtedy na szczęście nadeszła pora lunchu. Opowiedziałam o problemie nad talerzem naleśników, a jeden z kolegów zadał oczywiste w sumie pytanie – czy na pewno dobrze wyeskejpowałam (przepraszam!!!) wszystkie znaki specjalne. I wtedy wreszcie nadeszło olśnienie: nie, nie zrobiłam tego dobrze. Przeeskejpowałam je.

Znaki specjalne, takie jak d, w wyrażeniu regularnym oznaczające cyfrę, należy poprzedzić tzw. symbolem ucieczki, czyli w tym wypadku backslashem (ukośnikiem wstecznym). W łańcuchach znaków w kodzie Javy konieczne jest wprowadzenie dodatkowego backslasha, gdyż musimy jeszcze odebrać specjalne znaczenie samemu backslashowi (musimy poprzedzić znak ucieczki znakiem ucieczki…). Tyle razy widziałam te dwa ukośniki w parze, iż zupełnie zapomniałam o tym, iż w zewnętrznym pliku należy użyć tylko jednego!

Psikus 2: flagi

To bardzo proste. Załóżmy, iż wyrażenie nie ma brać pod uwagę wielkość liter. Normalnie oznaczamy to tak:

Pattern yearPattern = Pattern.compile("^\\d{2}[-/]\\d{2}\\s*w\\.$",Pattern.CASE_INSENSITIVE);

Świetnie, tylko jak przekazać tę flagę, jeżeli wyrażenie jest wczytywane z zewnątrz? Wychodzi na to, iż flagę, poprzedzoną znakiem zapytania, należy umieścić w nawiasie na początku wyrażenia. Ignorowanie wielkości liter (przy okazji, poznałam ostatnio nowe słowo – kasztowość) to literka i, zatem dodajemy (?i). Ostatecznie, w kodzie wyrażenie wygląda tak:

(?i)^\\d{2}[-/]\\d{2}\\s*w\\.$

a poza kodem, z pojedynczymi ukośnikami, tak:

(?i)^\d{2}[-/]\d{2}\s*w\.$

Psikus 3: String.replaceAll

W pewnym brzegowym przypadku mój kod, w wyniku wczytania wyrażeń regularnych z pliku, wykonywał operację, którą można w uproszczeniu zapisać tak:

String s = "stara treść".replaceAll(".*","nowa treść")

Po wykonaniu się tego kodu spodziewałam się, iż treść zostanie całkowicie zastąpiona, czyli wartością s będzie:

nowa treść

skoro * jest zachłannym kwantyfikatorem, to .* powinno dopasować się do całego napisu niezależnie od okoliczności.

Wyobraźcie sobie moje zaskoczenie (czy raczej przerażenie), gdy okazało się, iż s przyjęło wartość:

nowa treśćnowa treść

Próbowałam użyć jeszcze bardziej zaborczego kwantyfikatora *+, ale efekt był ten sam. Byłabym mniej zaskoczona, gdyby .* zostało dopasowane do każdej litery w łańcuchu. Jakim cudem dopasowało się dokładnie dwa razy?

Dalsze śledztwo wykazało, iż przebieg akcji jest następujący:

  1. Cały łańcuch dopasowuje się do .* i jest zamieniany na łańcuch “nowa treść”.
  2. Po dopasowaniu z oryginalnego łańcucha znaków nie zostaje nic, a raczej zostaje łańcuch "". Metoda replaceAll jeszcze raz sprawdza możliwość dopasowania i okazuje się, iż "" także pasuje do .*, zatem pusty łańcuch również zostaje wymieniony.
  3. Zasadniczo można by kontynuować i w nieskończoność dodawać na końcu "nowa zawartość", jednak na szczęście (?) dana pozycja w łańcuchu znaków jest traktowana jako sprawdzona i wykonanie metody kończy się.

Pozdrawiam siostry i braci w cierpieniu.

Idź do oryginalnego materiału