Unicode еквіваленти для \ w і \ b в регулярних виразах Java?


126

Багато сучасних реагеджезних інтерпретацій інтерпретують \wстенографію класу символів як "будь-яку букву, цифру або з'єднувальний розділовий знак" (зазвичай: підкреслення). Таким чином, регулярний вираз , як \w+сірники слова , як hello, élève, GOÄ_432або gefräßig.

На жаль, Java ні. У Java \wобмежується [A-Za-z0-9_]. Це ускладнює узгодження слів, подібних до вищезгаданих, серед інших проблем.

Виявляється також, що \bроздільник слів відповідає місцям, де він не повинен.

Який би був правильний еквівалент .NET-подібного, Unicode-відомого \wабо \bв Java? Які ще ярлики потрібно "переписати", щоб зробити їх Unicode?


3
Коротка історія, Тім, полягає в тому, що всі вони потребують написання, щоб привести їх у відповідність з Unicode. Я все ще не бачу ознак того, що Java 1.7 зробить щось більше з властивостями Unicode, ніж нарешті додасть підтримку сценаріїв, але це все. Є деякі речі, які ви дійсно не можете зробити без кращого доступу до повного комплексу властивостей Unicode. Якщо ви ще не маєте моїх скриптів uniprops та unicharsuninames ), вони приголомшують очі для відкриття очей у всьому цьому.
tchrist

Можна подумати про додавання знаків до класу слів. Так як наприклад & auml; може бути представлено в Unicode або як \ u0061 \ u0308 або \ u00E4.
Mostowski Згорнутись

3
Привіт, Тіме, перевіри моє оновлення. Вони додали прапор, щоб це все працювало. Ура!
tchrist

Відповіді:


240

Вихідний код

Вихідний код функцій переписування, про які я обговорюю нижче , доступний тут .

Оновлення в Java 7

Оновлений Patternклас Sun для JDK7 має чудовий новий прапор UNICODE_CHARACTER_CLASS, завдяки якому все працює знову правильно. Він доступний як вбудоване (?U)всередині візерунка, тому ви можете використовувати його також із Stringобгортками класу. Він також спортивно виправляв визначення для різних інших властивостей. Тепер він відстежує Unicode Standard, як RL1.2, так і RL1.2a з UTS № 18: Регулярні вирази Unicode . Це хвилююче і драматичне вдосконалення, і команду розробників слід похвалити за це важливе зусилля.


Проблеми Unicode Java у Regex

Проблема з Java регулярних виразів є те , що Perl 1.0 charclass вислизає - значення \w, \b, \s, \dі їх доповнень - не в Java поширюється на роботу з Unicode. Один з них \bкористується певною розширеною семантикою, але ці карти ні до \w, ні до ідентифікаторів Unicode , ні до властивостей Unicode розриву рядків .

Крім того, доступ до властивостей POSIX на Java доступний таким чином:

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}

Це справжній бардак, тому що це означає , що речі , як Alpha, Lowerі Spaceробити НЕ на мапі Java в Unicode Alphabetic, Lowercaseабо Whitespaceвластивості. Це надзвичайно дратує. Підтримка власності Unicode Java суворо є дворічною , тому я маю на увазі, що вона не підтримує властивості Unicode, яка з'явилася за останнє десятиліття.

Не в змозі правильно говорити про пробіл - це дуже дратує. Розглянемо наступну таблицю. Для кожної з цих точок коду є як стовпець J-результатів для Java, так і стовпець P-результатів для Perl або будь-якого іншого двигуна-регексу на основі 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  -

Бачиш це?

Практично кожен з цих результатів пробілів на Java - це "̲w̲r̲o̲n̲g̲" згідно Unicode. Це справді велика проблема. Java просто заплуталася, даючи відповіді, які є "неправильними" відповідно до існуючої практики, а також згідно з Unicode. Плюс Java навіть не дає вам доступу до реальних властивостей Unicode! Насправді Java не підтримує жодної властивості, яка відповідає пробілу Unicode.


Рішення всіх цих проблем та багато іншого

Щоб вирішити цю проблему та багато інших пов'язаних з цим проблем, я вчора написав функцію Java, щоб переписати рядок шаблону, яка переписує ці 14 втечі шарка:

\w \W \s \S \v \V \h \H \d \D \b \B \X \R

замінивши їх речами, які фактично працюють у відповідності Unicode передбачувано та послідовно. Це лише альфа-прототип з одного хак-сеансу, але він повністю функціональний.

Коротка історія полягає в тому, що мій код переписує ці 14 так:

\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*)

Деякі речі, які слід врахувати ...

  • Яка використовує для його \Xвизначення , що Unicode тепер посилається як спадщина графем кластера , а не як розширений кластера графеми , так як останній досить складніше. Зараз Perl використовує більш шалену версію, але стара версія все ще ідеально підходить для найпоширеніших ситуацій. EDIT: Див. Додаток внизу.

  • Що робити, \dзалежить від вашого наміру, але типовим є визначення Uniode. Я бачу людей, які не завжди хочуть \p{Nd}, але іноді [0-9]або \pN.

  • Два граничні визначення, \bі \B, спеціально написані для використання цього \wвизначення.

  • Це \wвизначення надто широке, тому що воно захоплює батьківські букви не лише обведені. Other_AlphabeticВластивість Unicode доступна лише до JDK7, тому це найкраще, що ви можете зробити.


Вивчення меж

Кордони були проблемою з тих пір, як Ларрі Уолл вперше вигадав \bі \Bсинтаксис для того, щоб говорити про них для Perl 1.0 ще в 1987 році. Ключовим моментом для розуміння того, як \bі \Bобох є робота, є розвіяння двох всепоглинаючих міфів про них:

  1. Вони шукають лише \wсимволів слова, ніколи не бувають символів, що не бувають .
  2. Вони спеціально не шукають краю струни.

А \bграничні кошти:

    IF does follow word
        THEN doesn't precede word
    ELSIF doesn't follow word
        THEN does precede word

І всі вони визначені абсолютно прямо як:

  • наступне слово є (?<=\w).
  • попереднє слово є (?=\w).
  • не слідує слово є (?<!\w).
  • не передує слову є (?!\w).

Тому, так як IF-THENкодується як and вид-разом ABв регулярних виразів, orце X|Y, і тому , що andвище , ніж в старшинства or, тобто просто AB|CD. Тож кожне, \bщо означає межу, можна сміливо замінити на:

    (?:(?<=\w)(?!\w)|(?<!\w)(?=\w))

з \wвизначеним відповідним чином.

(Вам може здатися дивним, що компоненти Aта Cкомпоненти - це протилежності. У ідеальному світі вам слід писати це AB|D, але я деякий час переслідував протиріччя взаємного виключення у властивостях Unicode - про що я думаю, що я подбав про це , але я залишив подвійну умову на кордоні про всяк випадок. Плюс це зробить її більш розширеною, якщо ви отримаєте додаткові ідеї пізніше.)

Для \Bне-меж логіка така:

    IF does follow word
        THEN does precede word
    ELSIF doesn't follow word
        THEN doesn't precede word

Дозволити всі екземпляри \Bзамінити на:

    (?:(?<=\w)(?=\w)|(?<!\w)(?!\w))

Це дійсно так \bі \Bповодиться. Для них є рівнозначні зразки

  • \bз використанням ((IF)THEN|ELSE)конструкції є(?(?<=\w)(?!\w)|(?=\w))
  • \Bз використанням ((IF)THEN|ELSE)конструкції є(?(?=\w)(?<=\w)|(?<!\w))

Але версії з просто AB|CDчудовими, особливо якщо вам не вистачає умовних зразків у мові регулярних виразів - як-от Java. ☹

Я вже перевірив поведінку меж, використовуючи всі три еквівалентні визначення за допомогою тестового набору, який перевіряє 110 385 408 відповідностей за пробіг, і який я запускав у десятках різних конфігурацій даних відповідно до:

     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)

Однак люди часто хочуть різного роду межі. Вони хочуть чогось, що відомо пробілу та межі:

  • лівий край як(?:(?<=^)|(?<=\s))
  • правий край як(?=$|\s)

Виправлення Java за допомогою Java

Код, який я розмістив у своїй іншій відповіді, надає це та ще ряд інших зручностей. Сюди входять визначення слів на природній мові, тире, дефіси та апострофи плюс ще трохи.

Це також дозволяє вказувати символи Unicode в логічних кодових точках, а не в ідіотичних сурогатах UTF-16. Важко перенапружити, як це важливо! І це лише для розширення рядків.

Для заміни шарж-класу з регулярними виразами, яка змушує шарклас у ваших Java-реджексах нарешті працювати на Unicode, і працюйте правильно, перейдіть на повне джерело звідси . Ви, звичайно, можете робити з цим як завгодно. Якщо ви виправляєте це, я хотів би почути це, але цього не потрібно. Це досить коротко. Кишки основної функції переписування регексу прості:

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;

У всякому разі, цей код - це лише альфа-реліз, що я зламав у вихідні. Це не залишиться таким.

Для бета-версії я маю намір:

  • скласти дублювання коду

  • забезпечити більш чіткий інтерфейс щодо нерозмірковування вхідних рядків у порівнянні з збільшенням утечек

  • забезпечити деяку гнучкість у \dрозширенні, а може і\b

  • надайте зручні методи, які допомагають обертатись та викликати Pattern.compile або String.matches або щось подібне для вас

Для випуску продукції він повинен мати javadoc та тестовий набір JUnit. Я можу включити свого гігатестера, але він не пишеться як тести JUnit.


Додаток

У мене є хороші та погані новини.

Хороша новина полягає в тому, що зараз я отримав дуже близьке наближення до розширеного кластеру графем, який можна використовувати для вдосконаленого \X.

Погана новина ☺ полягає в тому, що така закономірність:

(?:(?:\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:.))

що на Java ви б написали як:

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üß!


10
Це дивно. Дуже дякую.
Тім Піцкер

9
Христос, це просвітницька відповідь. Я тільки не отримую посилання на Джон Скіта. Що він стосується цього?
BalusC

12
@BalusC: Йон відповів раніше про те, що він дозволив мені задати питання. Але, будь ласка, не кидайте tв @tchrist. Це може піти мені в голову. :)
tchrist

3
Ви думали над тим, як додати це до OpenJDK?
Мартійн Вербург

2
@Martijn: Я ні, ні; Я не знав, що це "відкрито". :) Але я думав над тим, щоб звільнити це у більш формальному сенсі; інші в моєму відділі хочуть бачити, що це зроблено (з якоюсь ліцензією з відкритим кодом, можливо, BSD або ASL). Я, мабуть, збираюся змінити API від того, що він є в цьому альфа-прототипі, очистити код і т. Д. Але це надзвичайно допомагає нам , і ми вважаємо, що це теж допоможе іншим. Мені дуже хочеться, щоб Sun щось робив з їхньою бібліотекою, але Oracle вселяє впевненість у собі.
tchrist

15

Це справді прикро, що \wце не працює. Запропоноване рішення \p{Alpha}також не працює для мене.

Здається, [\p{L}]схоплює всі літери Unicode. Отже, еквівалент Unicode \wповинен бути [\p{L}\p{Digit}_].


Але \wтакож відповідає цифрам та ін. Я думаю, що лише для листів \p{L}спрацювало б
Тім Піцкер

Ти маєш рацію. \p{L}достатньо. Також я подумав, що проблемою є лише букви. [\p{L}\p{Digit}_]має вловлювати всі буквено-цифрові символи, включаючи підкреслення.
musiKk

@MusicKk: Дивіться мою відповідь на повне рішення, яке дозволяє нормально писати шаблони, але потім передайте їх через функцію, яка виправляє зенітні лакуни Java, щоб вона працювала належним чином на Unicode.
tchrist

Ні, \wUnicode визначається як набагато ширший, ніж просто, \pLі цифри ASCII від усіх дурних речей. Ви повинні написати, [\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]якщо ви хочете, що \wдля Java використовується Unicode - або ви можете просто використовувати мою unicode_charclassфункцію звідси . Вибачте!
tchrist

1
@Tim, так, для листів \pLце працює (вам не потрібно приймати однобуквенний реквізит). Однак ти рідко хочеш цього, тому що ти повинен бути досить обережним, щоб відповідність не отримала різних відповідей лише тому, що ваші дані знаходяться у формі нормалізації Unicode D (інакше NFD, що означає канонічне розкладання ), а не в NFC (NFD з наступним канонічним) композиція ). Прикладом є те, що кодова точка U + E9 ( "é") є \pLу формі NFC, але її форма NFD стає U + 65.301, тому відповідає \pL\pM. Ви можете щось обійти за допомогою \X:, (?:(?=\pL)\X)але вам знадобиться моя версія версії для Java. :(
tchrist

7

У Java, \wі \dвони не знають Unicode; вони відповідають лише символам ASCII [A-Za-z0-9_]та [0-9]. Те ж саме стосується \p{Alpha}і друзів ("символьні класи" POSIX, на яких вони базуються, повинні бути чутливими до локальної точки зору, але в Java вони лише коли-небудь відповідали символам ASCII). Якщо ви хочете відповідати "символам слова" Unicode, вам потрібно його прописати, наприклад [\pL\p{Mn}\p{Nd}\p{Pc}], для літер, модифікаторів без інтервалу (акценти), десяткових цифр та розділових знаків.

Однак у Java \b є розумним Unicode; він також використовує Character.isLetterOrDigit(ch)і перевіряє наголошені літери, але єдиний символ, що розпізнає розділові знаки, - це підкреслення. EDIT: коли я спробую ваш зразок коду, він друкує ""і élève"як слід ( дивіться його на ideone.com ).


Вибачте, Алан, але ти справді не можеш сказати, що Java - \bце кмітливість Unicode. Це робить тони і тонни помилок. "\u2163=", "\u24e7="і "\u0301="всі не узгоджуються з шаблоном "\\b="на Java, але повинні - як perl -le 'print /\b=/ || 0 for "\x{2163}=", "\x{24e7}=", "\x{301}="'виявляється. Однак якщо ви (і лише якщо) ви замінюєте мою версію кордону слів замість рідної \bна Java, то всі вони також працюють на Java.
tchrist

@tchrist: Я не коментував \bправильність, просто зазначив, що він працює на символах Unicode (як реалізовано в Java), а не тільки на ASCII як \wі на друзях. Однак він працює правильно щодо того, \u0301коли цей персонаж поєднується з базовим символом, як у e\u0301=. І я не переконаний, що Java в цьому випадку помиляється. Як комбінуючий знак можна вважати символом слова, якщо він не входить до групи графем з літерою?
Алан Мур

3
@Alan, це щось з'ясувалося, коли Unicode роз'яснював кластери графем, обговорюючи розширені та застарілі кластери графем. Старе визначення кластера графеми, в якому \Xозначає , що нема позначки, за якою слідує будь-яка кількість позначок, є проблематичним, тому що ви повинні мати можливість описати всі файли як відповідні /^(\X*\R)*\R?$/, але ви не можете, якщо у вас є \pMна початку файл або навіть рядок. Таким чином, вони поширили це, щоб завжди відповідати хоча б одному символу. Це було завжди, але тепер це змушує вищевказану схему працювати. [… Продовження…]
tchrist

2
@Alan, це більше шкоди, ніж користі, що уродженець Java \bчастково усвідомлює Unicode. Розглянемо відповідність рядка "élève"по шаблону \b(\w+)\b. Бачите проблему?
tchrist

1
@tchrist: Так, без меж слова \w+знаходить два збіги: lі ve, що досить погано. Але зі межами слова він нічого не знаходить, тому що \bрозпізнає éі èяк слова символів. Як мінімум, \bі \wслід погодитись, що є символом слова, а що ні.
Алан Мур
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.