Unicode эквиваленты для \ w и \ b в регулярных выражениях Java? - PullRequest
122 голосов
/ 29 ноября 2010

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

К сожалению, Java этого не делает. В Java \w ограничено [A-Za-z0-9_]. Это затрудняет сопоставление слов, подобных упомянутым выше, среди других проблем.

Также представляется, что разделитель слов \b совпадает в тех местах, где он не должен.

Что было бы правильным эквивалентом .NET-подобного, поддерживающего Unicode \w или \b в Java? Какие другие ярлыки нуждаются в «переписывании», чтобы они могли распознавать Unicode?

Ответы [ 3 ]

235 голосов
/ 29 ноября 2010

Исходный код

Исходный код для функций перезаписи, которые я обсуждаю ниже доступен здесь .

Обновление в Java 7

Обновленный класс Sun Pattern для JDK7 имеет чудесный новый флаг UNICODE_CHARACTER_CLASS, который заставляет все снова работать правильно. Он доступен как встраиваемый (?U) внутри шаблона, так что вы можете использовать его и с оболочками класса String. Это также спортивные исправленные определения для различных других свойств, также. Теперь он отслеживает стандарт Unicode как RL1.2 и RL1.2a из UTS # 18: Регулярные выражения Unicode . Это впечатляющее и впечатляющее улучшение, и команда разработчиков заслуживает похвалы за эту важную работу.


Проблемы с Java в Regex Unicode

Проблема с регулярными выражениями Java заключается в том, что экранирующий код Perl 1.0 - то есть \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 do not в 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 теперь использует более причудливую версию, но старая версия все еще отлично работает в самых распространенных ситуациях. РЕДАКТИРОВАТЬ: См. Приложение в нижней части.

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

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

  • Это определение \w является слишком широким, потому что оно захватывает заштрихованные буквы, а не только обведенные кружком. Свойство Unicode Other_Alphabetic недоступно до JDK7, так что это лучшее, что вы можете сделать.


Изучение границ

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

  1. Они только когда-либо ищут для \w символов слова, никогда для несловесных символов.
  2. Они не выглядят специальнодля края строки.

A \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 ed-вместе AB в регулярных выражениях or равно X|Y, а поскольку and имеет больший приоритет, чем or, то есть просто AB|CD.Таким образом, каждый \b, означающий, что границу можно безопасно заменить на:

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

с \w, определенным соответствующим образом.

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

Для \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.12

Я уже проверил поведение границ, используя все три эквивалентных определения с набором тестов, который проверяет 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

Код, который я разместил в Мой другой ответ , предоставляет этот и многие другие удобства.Это включает в себя определения слов, черточек, дефисов и апострофов на естественном языке, а также немного больше.

Также позволяет указывать символы Юникода в логических кодовых точках, а не в идиотских суррогатах UTF-16. Трудно переоценить, насколько это важно! И это только для расширения строки.

Для подстановки регулярных выражений в charclass, которая заставляет charclass в ваших регулярных выражениях Java finally работает на Unicode, и работает правильно, grab полный источник изздесь . Вы можете делать это, как вам угодно, конечно.Если вы исправите это, я хотел бы услышать об этом, но вы не обязаны это делать.Это довольно коротко.Суть основной функции переписывания регулярных выражений проста:

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

  • обеспечить конвенМетоды nce, которые обрабатывают поворот и вызов 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üß!

15 голосов
/ 29 ноября 2010

К сожалению, \w не работает.Предлагаемое решение \p{Alpha} также не работает для меня.

Кажется, [\p{L}] перехватывает все буквы Unicode.Таким образом, Unicode-эквивалент \w должен быть [\p{L}\p{Digit}_].

7 голосов
/ 29 ноября 2010

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

Однако, Java \b разбирается в Unicode;он использует Character.isLetterOrDigit(ch) и также проверяет наличие акцентированных букв, но единственный распознаваемый символ «соединительной пунктуации» - это подчеркивание. РЕДАКТИРОВАТЬ: когда я пробую ваш пример кода, он печатает "" и élève", как и должно ( см. Его на ideone.com ).

...