Эквиваленты 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 или \bJava? Какие еще ярлыки нужно «переписать», чтобы они поддерживали Unicode?

Тим Пицкер
источник
3
Вкратце, Тим, они все нуждаются в письме, чтобы привести их в соответствие с Unicode. Я до сих пор не вижу признаков того, что Java 1.7 будет делать что-то большее со свойствами Unicode, чем, наконец, добавит поддержку скриптов, но это все. Есть некоторые вещи, которые вы действительно не сможете сделать без лучшего доступа к полному набору свойств Unicode. Если у вас еще нет моих скриптов uniprops и unicharsunames ), они потрясающе открывают вам глаза на все это.
tchrist
Можно подумать о добавлении отметок к классу слов. Поскольку, например, & auml; может быть представлен в Юникоде как \ u0061 \ u0308 или \ u00E4.
Mostowski Collapse
3
Привет, Тим, зацени мое ОБНОВЛЕНИЕ. Они добавили флаг, чтобы все работало. Ура!
tchrist

Ответы:

240

Исходный код

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

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

Обновленный Patternкласс Sun для JDK7 имеет чудесный новый флаг UNICODE_CHARACTER_CLASS, который заставляет все снова работать правильно. Он доступен как встраиваемый (?U)для внутри шаблона, поэтому вы также можете использовать его с Stringоболочками класса. Также были исправлены определения для различных других свойств. Теперь он отслеживает стандарт Unicode в 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 - это «wr̲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. Они специально не ищут край веревки.

А \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-together 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

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

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

Для замены charclass регулярного выражения, которая заставляет charclass в ваших регулярных выражениях 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;

Во всяком случае, этот код - всего лишь альфа-версия, которую я взломал на выходных. Так не останется.

Для бета-тестирования я намерен:

  • сложите дублирование кода

  • обеспечить более понятный интерфейс, касающийся неэкранированных переходов строки по сравнению с расширяющими escape-символами регулярных выражений

  • обеспечить некоторую гибкость в \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üß!

tchrist
источник
10
Это потрясающе. Большое спасибо.
Тим Пицкер
9
Боже, это просвещенный ответ. Я не понимаю только упоминания Джона Скита. При чем тут он?
BalusC
12
@BalusC: Это ссылка на Джона, ранее говорившую, что он позволит мне задать вопрос. Но, пожалуйста, не tбросайте @tchrist. Это может ударить мне в голову. :)
tchrist
3
Вы думали о добавлении этого в OpenJDK?
Martijn Verburg
2
@Martijn: Нет, нет; Я не знал, что это было так «открыто». :) Но я думал о том, чтобы выпустить его в более формальном смысле; другие в моем отделе хотят, чтобы это было сделано (с какой-то лицензией с открытым исходным кодом, возможно, BSD или ASL). Я, вероятно, собираюсь изменить API из того, что есть в этом альфа-прототипе, очистить код и т. Д. Но он очень помогает нам , и мы полагаем, что он поможет и другим. Я действительно хотел бы, чтобы Sun что-нибудь сделала со своей библиотекой, но Oracle не вызывает доверия.
tchrist
15

Очень жаль, что \wэто не работает. Предлагаемое решение\p{Alpha} у меня тоже не работает.

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

Musikk
источник
Но \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}]]если хотите, чтобы \wJava поддерживала Unicode - или вы можете просто использовать мою unicode_charclassфункцию отсюда . Сожалею!
tchrist
1
@ Тим, да, для букв все- \pLтаки работает (однобуквенный реквизит использовать не обязательно). Однако вы редко этого хотите, потому что вы должны быть достаточно осторожны, чтобы ваш матч не получил разных ответов только потому, что ваши данные находятся в форме D нормализации Unicode (также известной как 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)и проверяет наличие букв с диакритическими знаками, но единственный «соединительный знак препинания», который он распознает, - это подчеркивание. РЕДАКТИРОВАТЬ: когда я пробую ваш образец кода, он распечатывается ""и é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следует договориться о том, что такое словесный символ, а что нет.
Алан Мур,