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ę, że 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.
Spis treści
Psikus 1: znaki ucieczki w wyrażeniach wczytywanych z zewnętrznego pliku
Załóżmy, że wyrażenie (które chcemy wczytać z zewnętrznego pliku) w zwykłym kodzie Javy wygląda tak:
1 |
Pattern yearPattern = Pattern.compile("^\\d{2}[-/]\\d{2}\\s*w\\.$"); |
Linia przedstawia zakres wieków, do którego dopasuje się na przykład linia:
1 |
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:
1 |
^\\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, że 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, że zupełnie zapomniałam o tym, że w zewnętrznym pliku należy użyć tylko jednego!
Psikus 2: flagi
To bardzo proste. Załóżmy, że wyrażenie nie ma brać pod uwagę wielkość liter. Normalnie oznaczamy to tak:
1 |
Pattern yearPattern = Pattern.compile("^\\d{2}[-/]\\d{2}\\s*w\\.$",Pattern.CASE_INSENSITIVE); |
Świetnie, tylko jak przekazać tę flagę, jeśli wyrażenie jest wczytywane z zewnątrz? Wychodzi na to, że 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:
1 |
(?i)^\\d{2}[-/]\\d{2}\\s*w\\.$ |
a poza kodem, z pojedynczymi ukośnikami, tak:
1 |
(?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:
1 |
String s = "stara treść".replaceAll(".*","nowa treść") |
Po wykonaniu się tego kodu spodziewałam się, że treść zostanie całkowicie zastąpiona, czyli wartością s
będzie:
1 |
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ę, że s
przyjęło wartość:
1 |
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, że 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
""
. MetodareplaceAll
jeszcze raz sprawdza możliwość dopasowania i okazuje się, że""
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.