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:
- Cały łańcuch dopasowuje się do .* i jest zamieniany na łańcuch “nowa treść”.
- 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.
- 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.