Kod źródłowy
Kod źródłowy funkcji przepisywania, które omówię poniżej, jest dostępny tutaj .
Aktualizacja w Javie 7
Zaktualizowana Pattern
klasa Sun dla JDK7 ma cudowną nową flagę UNICODE_CHARACTER_CLASS
, która sprawia, że wszystko znów działa poprawnie. Jest dostępny jako osadzalny element (?U)
wewnątrz wzorca, więc można go również używać z String
opakowaniami klasy. Zawiera również poprawione definicje różnych innych właściwości. Teraz śledzi standard Unicode, zarówno w RL1.2, jak i RL1.2a z UTS # 18: Wyrażenia regularne Unicode . To ekscytująca i radykalna poprawa, a zespół programistów zasługuje na pochwałę za ten ważny wysiłek.
Problemy z Regex Unicode w Javie
Problem z wyrażeniami regularnymi w Javie polega na tym, że klasa znaków Perl 1.0 ucieka - co oznacza \w
,\b
, \s
, \d
i ich uzupełnienia - nie są w Javie przedłużony do pracy z Unicode. Tylko jeden z nich \b
cieszy się pewną rozszerzoną semantyką, ale nie odwzorowują one ani na \w
, ani na identyfikatory Unicode , ani na właściwości podziału wiersza Unicode .
Dodatkowo do właściwości POSIX w Javie można uzyskać dostęp w następujący sposób:
POSIX syntax Java syntax
[[:Lower:]] \p{Lower}
[[:Upper:]] \p{Upper}
[[:ASCII:]] \p{ASCII}
[[:Alpha:]] \p{Alpha}
[[:Digit:]] \p{Digit}
[[:Alnum:]] \p{Alnum}
[[:Punct:]] \p{Punct}
[[:Graph:]] \p{Graph}
[[:Print:]] \p{Print}
[[:Blank:]] \p{Blank}
[[:Cntrl:]] \p{Cntrl}
[[:XDigit:]] \p{XDigit}
[[:Space:]] \p{Space}
To jest prawdziwy bałagan, bo to oznacza, że wszystko podoba Alpha
, Lower
iSpace
zrobić nie w mapie Java dla Unicode Alphabetic
, Lowercase
, lub Whitespace
właściwości. To jest wyjątkowo irytujące. Obsługa właściwości Unicode w Javie jest ściśle sprzed tysiąclecia , co oznacza, że nie obsługuje żadnej właściwości Unicode, która pojawiła się w ciągu ostatniej dekady.
Brak możliwości prawidłowego mówienia o białych znakach jest bardzo irytujący. Rozważ poniższą tabelę. Dla każdego z tych punktów kodowych istnieje zarówno kolumna wyników w języku J dla języka Java, jak i kolumna wyników P dla języka Perl lub dowolnego innego silnika wyrażeń regularnych opartego na PCRE:
Regex 001A 0085 00A0 2029
J P J P J P J P
\s 1 1 0 1 0 1 0 1
\pZ 0 0 0 0 1 1 1 1
\p{Zs} 0 0 0 0 1 1 0 0
\p{Space} 1 1 0 1 0 1 0 1
\p{Blank} 0 0 0 0 0 1 0 0
\p{Whitespace} - 1 - 1 - 1 - 1
\p{javaWhitespace} 1 - 0 - 0 - 1 -
\p{javaSpaceChar} 0 - 0 - 1 - 1 -
Zobaczyć, że?
Praktycznie każdy z tych wyników białych znaków Java jest zgodny z Unicode ̲w̲r̲o̲n̲g̲. To jest naprawdę duży problem. Java jest po prostu pomieszana, dając odpowiedzi, które są „błędne” zgodnie z istniejącą praktyką, a także zgodnie z Unicode. Plus Java nawet nie daje Ci dostępu do prawdziwych właściwości Unicode! W rzeczywistości Java nie obsługuje żadnej właściwości, która odpowiada białym znakom Unicode.
Rozwiązanie wszystkich tych problemów i nie tylko
Aby poradzić sobie z tym i wieloma innymi powiązanymi problemami, wczoraj napisałem funkcję Java, aby przepisać ciąg wzorca, który przepisuje te 14 znaków ucieczki klas:
\w \W \s \S \v \V \h \H \d \D \b \B \X \R
zastępując je rzeczami, które faktycznie działają, aby dopasować Unicode w przewidywalny i spójny sposób. To tylko prototyp alfa z jednej sesji hakerskiej, ale jest w pełni funkcjonalny.
Krótko mówiąc, mój kod przepisuje te 14 w następujący sposób:
\s => [\u0009-\u000D\u0020\u0085\u00A0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]
\S => [^\u0009-\u000D\u0020\u0085\u00A0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]
\v => [\u000A-\u000D\u0085\u2028\u2029]
\V => [^\u000A-\u000D\u0085\u2028\u2029]
\h => [\u0009\u0020\u00A0\u1680\u180E\u2000-\u200A\u202F\u205F\u3000]
\H => [^\u0009\u0020\u00A0\u1680\u180E\u2000\u2001-\u200A\u202F\u205F\u3000]
\w => [\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]
\W => [^\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]
\b => (?:(?<=[\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])(?![\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])|(?<![\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])(?=[\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]))
\B => (?:(?<=[\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])(?=[\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])|(?<![\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])(?![\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]))
\d => \p{Nd}
\D => \P{Nd}
\R => (?:(?>\u000D\u000A)|[\u000A\u000B\u000C\u000D\u0085\u2028\u2029])
\X => (?>\PM\pM*)
Kilka rzeczy do rozważenia ...
Który wykorzystuje do jej \X
definicji, co Unicode teraz odnosi się do postaci klastra spuścizna grafem , a nie rozszerzonym klastra grafem , jak ten ostatni jest raczej bardziej skomplikowana. Sam Perl używa teraz bardziej wyszukanej wersji, ale stara wersja jest nadal doskonale funkcjonalna w większości typowych sytuacji. EDYCJA: patrz dodatek na dole.
Co zrobić, \d
zależy od Twoich zamiarów, ale domyślną definicją jest Uniode. Widzę, że ludzie nie zawsze chcą \p{Nd}
, ale czasami albo [0-9]
albo \pN
.
Dwie definicje granic \b
i \B
są specjalnie napisane w celu użycia \w
definicji.
\w
Definicja ta jest zbyt szeroka, ponieważ obejmuje nie tylko litery zapisane w spreferze. Właściwość Unicode Other_Alphabetic
jest dostępna dopiero w JDK7, więc to najlepsze, co możesz zrobić.
Odkrywanie granic
Granice były problemem odkąd Larry Wall po raz pierwszy ukuł składnię \b
i \B
do mówienia o nich w Perlu 1.0 w 1987 roku. Klucz do zrozumienia, jak \b
i\B
obie działają, jest rozwianie dwóch wszechobecnych mitów na ich temat:
- Są tylko kiedykolwiek patrząc na
\w
znaki słowne, nigdy dla znaków non-słownych.
- Nie szukają specjalnie krawędzi sznurka.
A \b
brzegowe oznaczają:
IF does follow word
THEN doesn't precede word
ELSIF doesn't follow word
THEN does precede word
A to wszystko jest zdefiniowane w prosty sposób jako:
- podąża za słowem jest
(?<=\w)
.
- poprzedza słowo jest
(?=\w)
.
- nie wynika haseł IS
(?<!\w)
.
- nie poprzedza słowa jest
(?!\w)
.
Dlatego, skoro IF-THEN
jest kodowany jako and
ed-together AB
w wyrażeniach regularnych, to or
jest X|Y
, a ponieważ the and
ma wyższy priorytet niż or
, to jest po prostu AB|CD
. Więc każdy \b
, co oznacza granicę można bezpiecznie zastąpić:
(?:(?<=\w)(?!\w)|(?<!\w)(?=\w))
ze \w
zdefiniowanym w odpowiedni sposób.
(Możesz pomyśleć, że to dziwne, że komponenty A
i C
są przeciwieństwami. W idealnym świecie powinieneś być w stanie to napisać AB|D
, ale przez chwilę ścigałem wzajemne wykluczające się sprzeczności we właściwościach Unicode - co ja myślę, że się tym zająłem , ale na wszelki wypadek zostawiłem podwójny warunek w granicy. Dodatkowo, dzięki temu jest on bardziej rozszerzalny, jeśli później pojawią się dodatkowe pomysły.
W przypadku \B
braku granic logika jest następująca:
IF does follow word
THEN does precede word
ELSIF doesn't follow word
THEN doesn't precede word
Zezwalanie \B
na zastąpienie wszystkich wystąpień przez :
(?:(?<=\w)(?=\w)|(?<!\w)(?!\w))
To naprawdę jest jak \b
i \B
zachowuj się. Są dla nich równoważne wzory
\b
użycie ((IF)THEN|ELSE)
konstrukcji to(?(?<=\w)(?!\w)|(?=\w))
\B
użycie ((IF)THEN|ELSE)
konstrukcji to(?(?=\w)(?<=\w)|(?<!\w))
Ale wersje z just AB|CD
są w porządku, zwłaszcza jeśli brakuje wzorców warunkowych w Twoim języku regex - takim jak Java. ☹
Sprawdziłem już zachowanie granic przy użyciu wszystkich trzech równoważnych definicji za pomocą zestawu testów, który sprawdza 110 385 408 dopasowań na przebieg i który uruchomiłem na kilkunastu różnych konfiguracjach danych zgodnie z:
0 .. 7F the ASCII range
80 .. FF the non-ASCII Latin1 range
100 .. FFFF the non-Latin1 BMP (Basic Multilingual Plane) range
10000 .. 10FFFF the non-BMP portion of Unicode (the "astral" planes)
Jednak ludzie często chcą innego rodzaju granicy. Chcą czegoś, co jest świadome białych znaków i krawędzi łańcucha:
- lewa krawędź jak
(?:(?<=^)|(?<=\s))
- prawa krawędź jak
(?=$|\s)
Naprawianie Java za pomocą Java
Kod, który zamieściłem w mojej drugiej odpowiedzi zapewnia to i kilka innych udogodnień. Obejmuje to definicje słów, myślników, łączników i apostrofów w języku naturalnym, a także trochę więcej.
Pozwala także na określenie znaków Unicode w logicznych punktach kodowych, a nie w idiotycznych surogatach UTF-16. Trudno przecenić, jakie to ważne!A to tylko dla rozwinięcia ciągów.
Dla regex charclass podstawienie sprawia, że charclass w Javie regexes wreszcie pracę na Unicode, i działa prawidłowo, chwycić pełną źródło stąd . Możesz oczywiście zrobić z tym, co chcesz. Jeśli naprawisz to, chciałbym o tym usłyszeć, ale nie musisz. Jest dość krótki. Zalety głównej funkcji przepisywania wyrażeń regularnych są proste:
switch (code_point) {
case 'b': newstr.append(boundary);
break; /* switch */
case 'B': newstr.append(not_boundary);
break; /* switch */
case 'd': newstr.append(digits_charclass);
break; /* switch */
case 'D': newstr.append(not_digits_charclass);
break; /* switch */
case 'h': newstr.append(horizontal_whitespace_charclass);
break; /* switch */
case 'H': newstr.append(not_horizontal_whitespace_charclass);
break; /* switch */
case 'v': newstr.append(vertical_whitespace_charclass);
break; /* switch */
case 'V': newstr.append(not_vertical_whitespace_charclass);
break; /* switch */
case 'R': newstr.append(linebreak);
break; /* switch */
case 's': newstr.append(whitespace_charclass);
break; /* switch */
case 'S': newstr.append(not_whitespace_charclass);
break; /* switch */
case 'w': newstr.append(identifier_charclass);
break; /* switch */
case 'W': newstr.append(not_identifier_charclass);
break; /* switch */
case 'X': newstr.append(legacy_grapheme_cluster);
break; /* switch */
default: newstr.append('\\');
newstr.append(Character.toChars(code_point));
break; /* switch */
}
saw_backslash = false;
W każdym razie ten kod to tylko wydanie alfa, coś, co zhakowałem w weekend. Tak nie zostanie.
W przypadku wersji beta zamierzam:
złóż razem powielenie kodu
zapewniają jaśniejszy interfejs dotyczący znaków ucieczki ciągów bez zmiany znaczenia w porównaniu ze znakami ucieczki wyrażenia rozszerzającego
zapewniają pewną elastyczność w \d
rozszerzaniu, a być może\b
zapewniają wygodne metody, które obsługują odwracanie i wywoływanie Pattern.compile lub String.matches lub co innego
W przypadku wydania produkcyjnego powinien mieć javadoc i zestaw testów JUnit. Mogę dołączyć mój gigatester, ale nie jest to napisane jako testy JUnit.
Uzupełnienie
Mam dobre i złe wieści.
Dobra wiadomość jest taka, że mam teraz bardzo bliskie przybliżenie do rozszerzonego klastra grafemowego, którego można użyć do ulepszenia \X
.
Zła wiadomość ☺ jest taka, że ten wzór jest następujący:
(?:(?:\u000D\u000A)|(?:[\u0E40\u0E41\u0E42\u0E43\u0E44\u0EC0\u0EC1\u0EC2\u0EC3\u0EC4\uAAB5\uAAB6\uAAB9\uAABB\uAABC]*(?:[\u1100-\u115F\uA960-\uA97C]+|([\u1100-\u115F\uA960-\uA97C]*((?:[[\u1160-\u11A2\uD7B0-\uD7C6][\uAC00\uAC1C\uAC38]][\u1160-\u11A2\uD7B0-\uD7C6]*|[\uAC01\uAC02\uAC03\uAC04])[\u11A8-\u11F9\uD7CB-\uD7FB]*))|[\u11A8-\u11F9\uD7CB-\uD7FB]+|[^[\p{Zl}\p{Zp}\p{Cc}\p{Cf}&&[^\u000D\u000A\u200C\u200D]]\u000D\u000A])[[\p{Mn}\p{Me}\u200C\u200D\u0488\u0489\u20DD\u20DE\u20DF\u20E0\u20E2\u20E3\u20E4\uA670\uA671\uA672\uFF9E\uFF9F][\p{Mc}\u0E30\u0E32\u0E33\u0E45\u0EB0\u0EB2\u0EB3]]*)|(?s:.))
które w Javie napiszesz jako:
String extended_grapheme_cluster = "(?:(?:\\u000D\\u000A)|(?:[\\u0E40\\u0E41\\u0E42\\u0E43\\u0E44\\u0EC0\\u0EC1\\u0EC2\\u0EC3\\u0EC4\\uAAB5\\uAAB6\\uAAB9\\uAABB\\uAABC]*(?:[\\u1100-\\u115F\\uA960-\\uA97C]+|([\\u1100-\\u115F\\uA960-\\uA97C]*((?:[[\\u1160-\\u11A2\\uD7B0-\\uD7C6][\\uAC00\\uAC1C\\uAC38]][\\u1160-\\u11A2\\uD7B0-\\uD7C6]*|[\\uAC01\\uAC02\\uAC03\\uAC04])[\\u11A8-\\u11F9\\uD7CB-\\uD7FB]*))|[\\u11A8-\\u11F9\\uD7CB-\\uD7FB]+|[^[\\p{Zl}\\p{Zp}\\p{Cc}\\p{Cf}&&[^\\u000D\\u000A\\u200C\\u200D]]\\u000D\\u000A])[[\\p{Mn}\\p{Me}\\u200C\\u200D\\u0488\\u0489\\u20DD\\u20DE\\u20DF\\u20E0\\u20E2\\u20E3\\u20E4\\uA670\\uA671\\uA672\\uFF9E\\uFF9F][\\p{Mc}\\u0E30\\u0E32\\u0E33\\u0E45\\u0EB0\\u0EB2\\u0EB3]]*)|(?s:.))";
¡Tschüß!