Ruby – Wyrażenia regularne a znaki specjalne

„There ain’t no such thing as plain text”. Joel Spolsky (what every developer must know about unicode).
„Nie istnieje nic takiego jak czysty tekst”.

Do dalszych rozważań  korzystam z systemu używającego kodowania UTF-8 oraz Ruby 2.0. Rezultaty mogą być inne jeśli użyje się innego kodowania.

Ostatnio parsowałem trochę tekstu polskiego. Jako że wyrażenia regularne w każdym języku działają trochę inaczej jeśli chodzi o zestaw znaków większy niż ASCII (w PHP: /u), opisałem te scenariusze w Rubym.

Polskie znaki

Najpierw zobaczmy, jak Polskie znaki są reprezentowane w stringu:

[11] pry(main)> "test".bytes.to_a
=> [116, 101, 115, 116]
[12] pry(main)> "teść".bytes.to_a
=> [116, 101, 197, 155, 196, 135]

Locale w moim systemie to LANG=pl_PL.UTF-8. Jak widać, zarówno „ś” jak i „ć” zajęły dwa bajty (w UTF-8 może być maksymalnie 6 bajtów na znak). Zobaczmy dwa wyrażenia:

[5] pry(main)> /\w+/.match('test')
=> #<MatchData "test">
[6] pry(main)> /\w+/.match('teść')
=> #<MatchData "te">

zgodnie z oczekiwaniami, ść przysporzyło trochę kłopotu. „\w” nie dopasowuje polskich znaków. Rozwiązaniem może być użycie klasy POSIX dopasowującej znaki z alfabetu:

[8] pry(main)> /[[:alpha:]]+/.match('teść')
=> #<MatchData "teść">

Można także wykorzystać „character properties„, które opisane są także tutaj.

[9] pry(main)> /\p{Word}+/.match('teść')
=> #<MatchData "teść">

Białe znaki

Jeśli tekst pochodzi z edytora tekstu lub z internetu, nie należy zakładać, że „\s” dopasuje każdą spację 😉 Otóż, jak się okazuje:

[1] pry(main)> /\w+\s\w+/.match "word word" # zwykła spacja
=> #<MatchData "word word">

[2] pry(main)> /\w+\s\w+/.match "word word" # twarda spacja
=> nil

w drugimi przypadku do oddzielenia słów użyta została twarda spacja. W Windowsie można ją wstawić poprzez przystrzymanie alt+0160 (klawiatura numeryczna), w VIM przez naciśnięcie <C-k> <spacja> <spacja>.
W każdym razie, żeby rozwiązać problem można albo zamienić wszystkie twarde spacje na spacje w stringu, lub zmienić wyrażenie regularne coby wykorzystywało klasę POSIX [[:space:]], można także dopisać twardą spację do definicji grupy wyrażenia regularnego:

[3] pry(main)> /\w+[[:space:]]\w+/.match "word word" # twarda spacja
=> #<MatchData "word word">
# or
[4] pry(main)> /\w+[\s ]\w+/.match "word word"
=> #<MatchData "word word">

 

Leave a Reply