1. Обзор
Многие алфавиты содержат ударения и диакритические знаки. Для надежного поиска или индексации данных нам может понадобиться преобразовать строку с диакритическими знаками в строку, содержащую только символы ASCII. Unicode определяет процедуру нормализации текста, которая помогает сделать это.
В этом уроке мы увидим, что такое нормализация текста Unicode, как мы можем использовать ее для удаления диакритических знаков, а также подводные камни, которых следует остерегаться. Затем мы увидим несколько примеров с использованием класса Java Normalizer
и Apache Commons StringUtils.
2. Краткий обзор проблемы
Допустим, мы работаем с текстом, содержащим диапазон диакритических знаков, которые мы хотим удалить:
āăąēîïĩíĝġńñšŝśûůŷ
Прочитав эту статью, мы узнаем, как избавиться от диакритических знаков и в итоге получить:
aaaeiiiiggnnsssuuy
3. Основы Юникода
Прежде чем перейти непосредственно к коду, давайте изучим некоторые основы Unicode.
Для представления символа с диакритическим знаком или знаком ударения в Unicode могут использоваться различные последовательности кодовых точек. Причиной этого является историческая совместимость со старыми наборами символов.
Нормализация Unicode — это декомпозиция символов с использованием форм эквивалентности, определенных стандартом .
3.1. Эквивалентные формы Unicode
Для сравнения последовательностей кодовых точек Unicode определяет два термина: каноническая эквивалентность
и совместимость
.
Канонически эквивалентные кодовые точки имеют одинаковый внешний вид и значение при отображении. Например, буква «ś» (латинская буква «s» с острым знаком) может быть представлена одной кодовой точкой +U015B или двумя кодовыми точками +U0073 (латинская буква «s») и +U0301 (символ остроты).
С другой стороны, совместимые последовательности могут иметь разный внешний вид, но одинаковое значение в некоторых контекстах. Например, кодовая точка +U013F (латинская лигатура «Ŀ») совместима с последовательностью +U004C (латинская буква «L») и +U00B7 (символ «·»). Более того, некоторые шрифты могут отображать среднюю точку внутри буквы L, а некоторые — после нее.
Канонически эквивалентные последовательности совместимы, но не всегда верно обратное.
3.2. Разложение персонажа
Декомпозиция символов заменяет составной символ кодовыми точками базовой буквы с последующим объединением символов (согласно форме эквивалентности). Например, эта процедура разложит букву «а» на символы «а» и «-».
3.3. Соответствие диакритическим знакам и знакам ударения
Как только мы отделили базовый символ от диакритического знака, мы должны создать выражение, соответствующее нежелательным символам. Мы можем использовать либо блок символов, либо категорию.
Наиболее популярным блоком кода Unicode является Combining Diacritical Marks
. Он не очень большой и содержит всего 112 наиболее часто встречающихся комбинируемых символов. С другой стороны, мы также можем использовать категорию Unicode Mark
. Он состоит из кодовых точек, которые объединяют метки и делятся на три подкатегории:
Nonspacing_Mark
: эта категория включает 1839 кодовых точек.Enclosing_Mark
: содержит 13 кодовых точек.Spacing_Combining_Mark
: содержит 443 точки.
Основное различие между блоком символов Unicode и категорией заключается в том, что блок символов содержит непрерывный диапазон символов. С другой стороны, категория может иметь много блоков символов. Например, это как раз случай комбинирования диакритических знаков
: все кодовые точки, принадлежащие этому блоку, также включены в категорию Nonspacing_Mark
.
4. Алгоритм
Теперь, когда мы понимаем основные термины Unicode, мы можем спланировать алгоритм удаления диакритических знаков из String
.
Сначала мы отделим базовые символы от акцентных и диакритических знаков с помощью класса Normalizer
. Более того, мы выполним декомпозицию совместимости, представленную в виде перечисления Java NFKD
. Кроме того, мы используем декомпозицию совместимости, потому что она разлагает больше лигатур, чем канонический метод (например, лигатура «fi»).
Во- вторых, мы удалим все символы, соответствующие категории Unicode Mark
, используя регулярное выражение \p{M}
. Мы выбрали эту категорию, потому что она предлагает самый широкий диапазон оценок.
5. Использование ядра Java
Давайте начнем с нескольких примеров использования ядра Java.
5.1. Проверить, нормализована ли строка
Прежде чем мы выполним нормализацию, мы можем захотеть проверить, что String
еще не нормализована:
assertFalse(Normalizer.isNormalized("āăąēîïĩíĝġńñšŝśûůŷ", Normalizer.Form.NFKD));
5.2. Разложение строки
Если наша строка
не нормализована, мы переходим к следующему шагу. Чтобы отделить символы ASCII от диакритических знаков, мы выполним нормализацию текста Unicode, используя декомпозицию совместимости:
private static String normalize(String input) {
return input == null ? null : Normalizer.normalize(input, Normalizer.Form.NFKD);
}
После этого шага обе буквы «â» и «ä» будут сокращены до «a», за которыми следуют соответствующие диакритические знаки.
5.3. Удаление кодовых точек, представляющих диакритические знаки и знаки ударения
После того, как мы разложили нашу строку
, мы хотим удалить ненужные кодовые точки. Поэтому мы будем использовать регулярное выражение Unicode \p{M}
:
static String removeAccents(String input) {
return normalize(input).replaceAll("\\p{M}", "");
}
5.4. Тесты
Давайте посмотрим, как наша декомпозиция работает на практике. Во-первых, давайте выберем символы, имеющие форму нормализации, определенную Unicode, и рассчитываем удалить все диакритические знаки:
@Test
void givenStringWithDecomposableUnicodeCharacters_whenRemoveAccents_thenReturnASCIIString() {
assertEquals("aaaeiiiiggnnsssuuy", StringNormalizer.removeAccents("āăąēîïĩíĝġńñšŝśûůŷ"));
}
Во-вторых, давайте выберем несколько символов без отображения декомпозиции:
@Test
void givenStringWithNondecomposableUnicodeCharacters_whenRemoveAccents_thenReturnOriginalString() {
assertEquals("łđħœ", StringNormalizer.removeAccents("łđħœ"));
}
Как и ожидалось, наш метод не смог их разложить.
Кроме того, мы можем создать тест для проверки шестнадцатеричных кодов разложенных символов:
@Test
void givenStringWithDecomposableUnicodeCharacters_whenUnicodeValueOfNormalizedString_thenReturnUnicodeValue() {
assertEquals("\\u0066 \\u0069", StringNormalizer.unicodeValueOfNormalizedString("fi"));
assertEquals("\\u0061 \\u0304", StringNormalizer.unicodeValueOfNormalizedString("ā"));
assertEquals("\\u0069 \\u0308", StringNormalizer.unicodeValueOfNormalizedString("ï"));
assertEquals("\\u006e \\u0301", StringNormalizer.unicodeValueOfNormalizedString("ń"));
}
5.5. Сравните строки, включая диакритические знаки, используя сортировщик
Пакет java.text
включает в себя еще один интересный класс — Collator
. Это позволяет нам выполнять сравнения строк
с учетом региональных настроек . Важным свойством конфигурации является сила Collator
. Это свойство определяет минимальный уровень различия, который считается значимым при сравнении.
Java предоставляет четыре значения прочности для Collator
:
PRIMARY
: сравнение без учета регистра и ударения.SECONDARY
: сравнение без регистра, но с акцентами и диакритическими знаками.ТРЕТИЙ
: сравнение, включая регистр и акцентыИДЕНТИЧНО
: все различия значимы
Давайте проверим несколько примеров, сначала с основной силой:
Collator collator = Collator.getInstance();
collator.setDecomposition(2);
collator.setStrength(0);
assertEquals(0, collator.compare("a", "a"));
assertEquals(0, collator.compare("ä", "a"));
assertEquals(0, collator.compare("A", "a"));
assertEquals(1, collator.compare("b", "a"));
Вторичная сила включает чувствительность к акценту:
collator.setStrength(1);
assertEquals(1, collator.compare("ä", "a"));
assertEquals(1, collator.compare("b", "a"));
assertEquals(0, collator.compare("A", "a"));
assertEquals(0, collator.compare("a", "a"));
Третичная прочность включает случай:
collator.setStrength(2);
assertEquals(1, collator.compare("A", "a"));
assertEquals(1, collator.compare("ä", "a"));
assertEquals(1, collator.compare("b", "a"));
assertEquals(0, collator.compare("a", "a"));
assertEquals(0, collator.compare(valueOf(toChars(0x0001)), valueOf(toChars(0x0002))));
Одинаковая сила делает важными все различия. Предпоследний пример интересен тем, что мы можем обнаружить разницу между управляющими кодовыми точками Unicode +U001 (код для «Начала заголовка») и +U002 («Начало текста»):
collator.setStrength(3);
assertEquals(1, collator.compare("A", "a"));
assertEquals(1, collator.compare("ä", "a"));
assertEquals(1, collator.compare("b", "a"));
assertEquals(-1, collator.compare(valueOf(toChars(0x0001)), valueOf(toChars(0x0002))));
assertEquals(0, collator.compare("a", "a")));
Один последний пример, который стоит упомянуть, показывает, что если символ не имеет определенного правила разложения, он не будет считаться равным другому символу с той же базовой буквой . Это связано с тем, что Collator
не сможет выполнить декомпозицию Unicode :
collator.setStrength(0);
assertEquals(1, collator.compare("ł", "l"));
assertEquals(1, collator.compare("ø", "o"));
6. Использование Apache Commons StringUtils
Теперь, когда мы увидели, как использовать ядро Java для удаления акцентов, мы проверим, что предлагает Apache Commons Text . Как мы скоро узнаем, его проще использовать, но у нас меньше контроля над процессом декомпозиции . Под капотом используется метод Normalizer.normalize()
с формой декомпозиции NFD
и регулярным выражением \p{InCombiningDiacriticalMarks}:
static String removeAccentsWithApacheCommons(String input) {
return StringUtils.stripAccents(input);
}
6.1. Тесты
Давайте посмотрим на этот метод на практике — сначала только с разложимыми символами Unicode :
@Test
void givenStringWithDecomposableUnicodeCharacters_whenRemoveAccentsWithApacheCommons_thenReturnASCIIString() {
assertEquals("aaaeiiiiggnnsssuuy", StringNormalizer.removeAccentsWithApacheCommons("āăąēîïĩíĝġńñšŝśûůŷ"));
}
Как и ожидалось, мы избавились от всех акцентов.
Попробуем строку , содержащую лигатуру и буквы со штрихом :
@Test
void givenStringWithNondecomposableUnicodeCharacters_whenRemoveAccentsWithApacheCommons_thenReturnModifiedString() {
assertEquals("lđħœ", StringNormalizer.removeAccentsWithApacheCommons("łđħœ"));
}
Как мы видим, метод StringUtils.stripAccents()
вручную определяет правило перевода для латинских символов ł и Ł. Но, к сожалению, другие лигатуры он не нормализует .
7. Ограничения декомпозиции символов в Java
Подводя итог, мы увидели, что некоторые символы не имеют определенных правил декомпозиции. В частности, Unicode не определяет правила декомпозиции для лигатур и символов со штрихом . Из-за этого Java также не сможет их нормализовать. Если мы хотим избавиться от этих символов, мы должны определить сопоставление транскрипции вручную.
Наконец, стоит подумать, нужно ли нам избавляться от ударений и диакритических знаков. Для некоторых языков буква, лишенная диакритических знаков, не имеет особого смысла. В таких случаях лучше использовать класс Collator
и сравнивать две строки
, включая информацию о локали.
8. Заключение
В этой статье мы рассмотрели удаление акцентов и диакритических знаков с помощью ядра Java и популярной служебной библиотеки Java Apache Commons. Мы также увидели несколько примеров и узнали, как сравнивать текст, содержащий диакритические знаки, а также кое-что, на что следует обратить внимание при работе с текстом, содержащим диакритические знаки.
Как всегда, полный исходный код статьи доступен на GitHub .