Юникодные эквиваленты для \w и \b в регулярных выражениях Java?
Многие современные реализации регулярных выражений интерпретируют \w
Сокращение класса символов в виде "любой буквы, цифры или соединительной пунктуации" (обычно: подчеркивание). Таким образом, регулярное выражение, как \w+
соответствует словам как hello
, élève
, GOÄ_432
или же gefräßig
,
К сожалению, Java этого не делает. В Java \w
ограничено [A-Za-z0-9_]
, Это затрудняет сопоставление слов, подобных упомянутым выше, среди других проблем.
Также кажется, что \b
разделитель слов совпадает в местах, где это не должно быть.
Что было бы правильным эквивалентом.NET-подобного, Unicode-осведомленного \w
или же \b
на яве? Какие другие ярлыки нуждаются в "переписывании", чтобы они знали Unicode?
3 ответа
Исходный код
Исходный код для функций переписывания, которые я обсуждаю ниже , доступен здесь.
Обновление в Java 7
Солнце обновлено Pattern
класс для JDK7 имеет новый чудесный флаг, UNICODE_CHARACTER_CLASS
, что заставляет все снова работать правильно. Это доступно как встраиваемый (?U)
внутри шаблона, так что вы можете использовать его с String
фантики класса тоже. Это также спортивные исправленные определения для различных других свойств, также. Теперь он отслеживает стандарт Unicode в RL1.2 и RL1.2a из UTS#18: Регулярные выражения Unicode. Это захватывающее и значительное улучшение, и команда разработчиков заслуживает похвалы за эти важные усилия.
Проблемы Java с регулярным выражением 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
не в Java сопоставлять с Юникодом 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 является пустым, согласно 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
определение слишком широкое, потому что оно захватывает пареннированные буквы, а не только окруженные. ЮникодOther_Alphabetic
свойство недоступно до JDK7, так что это лучшее, что вы можете сделать.
Изучение границ
Границы были проблемой с тех пор, как Ларри Уолл впервые придумал \b
а также \B
Синтаксис говорить о них для Perl 1.0 еще в 1987 году. Ключ к пониманию того, как \b
а также \B
Обе работы должны развеять два распространенных мифа о них:
- Они только ищут
\w
символы слова, никогда не для не-словесных символов. - Они специально не ищут края струны.
\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
, но какое-то время я преследовал противоречия взаимного исключения в свойствах Юникода - о которых, я думаю, я позаботился, но на всякий случай я оставил двойное условие в границе. Плюс это делает его более расширяемым, если вы получите дополнительные идеи позже.)
Для \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. Трудно переоценить, насколько это важно! И это только для расширения строки.
Для подстановки регулярных выражений в 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;
В любом случае, этот код - просто альфа-версия, которую я взломал на выходных. Так не будет.
Для беты я намерен:
сложить дублирование кода
обеспечить более понятный интерфейс относительно экранирования неэкранированной строки по сравнению с экранированием регулярного выражения
обеспечить некоторую гибкость в
\d
расширение, и, возможно,\b
предоставить удобные методы, которые обрабатывают поворот и вызов Pattern.compile или String.matches или еще много чего для вас
Для производственной версии он должен иметь Javadoc и набор тестов JUnit. Я могу включить мой gigatester, но он не написан как тесты 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üß!
Очень жаль, что \w
не работает Предлагаемое решение \p{Alpha}
у меня тоже не работает
Похоже на то [\p{L}]
ловит все буквы Unicode. Таким образом, Unicode эквивалент \w
должно быть [\p{L}\p{Digit}_]
,
В 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).