Главная Обратная связь

Дисциплины:






Нормализация Unicode-строк



До сих пор мы пользовались монолитными символами, в которых базовый символ и диакритический знак объединены в одну кодовую позицию. Но, вообще говоря, в Unicode символы и диакритические знаки представлены отдельно. Вместо того чтобы хранить букву é в кодовой позиции СТРОЧНАЯ ЛАТИНСКАЯ БУКВА E С АКУТОМ, можно было бы представить ее в составной форме как СТРОЧНУЮ ЛАТИНСКУЮ БУКВУ E и МОДИФИЦИРУЮЩИЙ АКУТ.

Для чего это может понадобиться? Для обеспечения дополнительной гибкости и возможности применять диакритические знаки к любому символу, а не ограничивать себя комбинациями, которые предусмотрел проектировщик кодировки. На самом деле в шрифты включены глифы для наиболее распространенных комбинаций символа и диакритического знака, но отображение символа и его кодирование — вещи разные.

При проектировании Unicode приходилось учитывать такие вещи, как эффективность и совместимость с существующими национальными кодировками. Иногда это приводит к избыточности; например, в Unicode имеются кодовые позиции как для составных форм, так и для многих уже применяющихся монолитных форм.

Рассмотрим, к примеру, немецкое слово «öffnen» (открывать). Даже если забыть о регистре, его можно закодировать четырьмя способами:

1. о + МОДИФИЦИРУЮЩАЯ ТРЕМА (u+0308) +f+f+n+e+n

2. СТРОЧНАЯ ЛАТИНСКАЯ БУКВА О С ТРЕМОЙ (U+00F6) + f + f + n + е + n

3. о + МОДИФИЦИРУЮЩАЯ ТРЕМА + ЛИГАТУРА ДВОЙНОЕ F (U+FB00) + n + е + n.

4. СТРОЧНАЯ ЛАТИНСКАЯ БУКВА О С ТРЕМОЙ + ЛИГАТУРА ДВОЙНОЕ F + n + e + n

Трема — это две точки над буквой (в немецком языке называется «умляут»).

Нормализацией называется процедура приведения разных представлений символа к стандартной форме. Можно быть уверенным, что после нормализации данный символ закодирован вполне определенным образом. Каким именно, зависит оттого, чего мы хотим достичь. В приложении 15 к стандарту Unicode перечислены четыре формы нормализации:

1. Форма D (каноническая декомпозиция).

2. Форма С (каноническая декомпозиция с последующей канонической композицией).

3. Форма KD (совместимая декомпозиция).

4. Форма KC (совместимая декомпозиция с последующей канонической композицией).

Иногда можно встретить аббревиатуры NKFC (Normalization Form KC) и т.д.

Точные правила, сформулированные в стандарте, довольно сложны; в них проведено различие между «канонической эквивалентностью» и «совместимой эквивалентностью». (Корейский и японский языки требуют особого рассмотрения, но мы не станем тратить на это время.) В таблице 4.2 показано, как форма нормализации влияет на приведенные выше строки.

 

Таблица 4.2. Нормализованные формы в Unicode



Исходная NFD NFC NFKD NFKC
o+ ̈+f+f+n+e+n o+ ̈+f+f+n+e+n ö+f+f+n+e+n o+ ̈+f+f+n+e+n ö+f+f+n+e+n
ö+f+f+n+e+n o+ ̈+f+f+n+e+n ö+f+f+n+e+n o+ ̈+f+f+n+e+n ö+f+f+n+e+n
o+ ̈+ff+n+e+n o+ ̈+ff+n+e+n ö+ff+n+e+n o+ ̈+f+f+n+e+n ö+f+f+n+e+n
ö+ff+n+e+n o+ ̈+ff+n+e+n ö+ff+n+e+n o+ ̈+f+f+n+e+n ö+f+f+n+e+n

Формы С и D обратимы, KC и KD — нет. С другой стороны, потеря некоторых данных в формах KC и KD — свидетельство того, что все четыре строки двоично эквивалентны. Какая форма лучше всего подходит, зависит от приложения. Мы ещё вернемся к этой теме в следующем разделе.

Для Ruby есть библиотека, позволяющая выполнить описанные нормализации, хотя в стандартный дистрибутив она не входит. Вы можете скачать ее со страницы http://www.yoshidam.net/Ruby.html и установить командой gem install Unicode.

Если библиотека Unicode установлена, то для выполнения любой нормализации достаточно вызвать один из методов Unicode.normalize_x:

require 'Unicode'

sword_kd = Unicode.normalize_KD(sword)

sword_kd.scan(/./) # ["e", "'", "p", "e", "'", "e"]

sword_kc = Unicode.normalize_KC(sword)

sword_kc.scan(/./) # [ "é", "p", "é", "e"]

Упорядочение строк

Обычно, хотя и не всегда, строки упорядочиваются по алфавиту или сходным образом. Упорядочение тесно связано с нормализацией: в обоих случаях применяются одни и те же идеи и библиотеки.

Предположим, например, что мы хотим отсортировать такой массив строк:

eacute = [0x00Е9].pack('U')

acute = [0x0301].pack('U')

array = ["epicurian", "#{eacute}p#{eacute}e", "e#{acute}lan"]

# ["epicurian", "éрéе", "élan"]

Что произойдет, если передать этот массив методу Array#sort?

array.sort # ["epicurian", "élan", "éрéе"]

He годится!.. Попытаемся понять, почему так получилось. Сортируемые строки Ruby сравнивает побайтно. Чтобы убедиться в этом, достаточно взглянуть на первые несколько байтов каждой строки:

array.map {|item| "#{item}: #{item.unpack('С*')[0,3].join(',')}" }

# ["epicurian: 101,112,105", "éрéе: 195,169,112",

# "élan: 101,204,129"]

Тут возникают две трудности. Во-первых, символы UTF-8, не имеющие аналога в кодировке ASCII, начинаются с байта, имеющего большое числовое значение, а стало быть, после сортировки неизбежно окажутся после ASCII-символов. Во-вторых, составные латинские символы оказываются раньше монолитных из-за первого ASCII-байта.

В системные библиотеки обычно включают функции сортировки, которые сравнивают строки в соответствии с правилами конкретного языка. В библиотеке, поставляемой вместе с компилятором языка С, для этого служат функции strxfrm и strcoll.

Имейте в виду, что проблема возникает даже в случае кодировки ASCII. При сортировке ASCII-строк в Ruby производится прямое лексикографическое сравнение, однако в реальной жизни (например, если мы хотим отсортировать по названиям книги из библиотеки Конгресса США) есть много правил, которые не учитываются при таком упрощенном подходе.

Для упорядочения строк можно создать промежуточные строки и отсортировать именно их. Как конкретно это сделать, зависит от предъявляемых требований и языка; универсального алгоритма не существует.

Предположим, что список обрабатывается согласно правилам английского языка, причем диакритические знаки игнорируются. Первым делом нужно определить методику трансформации. Мы приведем все символы к составному виду, а затем исключим диакритические знаки, оставив только базовые символы. Для модифицирующих диакритических знаков в Unicode выделен диапазон отU+0300 to U+036F:

def transform(str)

Unicode.normalize_KD(str).unpack('U*').select{ |cp|

cp < 0x0300 || cp > 0x036F

}.pack('U*')

end

array.map{|x| transform(x) } # ["epicurian", "epee", "elan"]

Затем создадим хэшированную таблицу, чтобы установить соответствие между исходными и трансформированными строками, и воспользуемся ей для сортировки исходных строк. Наличие такой таблицы позволяет провести трансформацию только один раз.

def collate(array)

transformations = array.inject({}) do |hash, item|

hash[item] = yield item

hash

end

array.sort_by {|x| transformations[x] }

end

 

collate(array) {|a| transform(a) } # ["élan", "épée", "epicurian"]

Уже лучше, но мы еще не учли прописные буквы и эквивалентность символов. Возьмем для примера немецкий язык.

На самом деле в немецком языке есть несколько способов упорядочения; мы остановимся на стандарте DIN-2 (как в телефонном справочнике). Согласно этому стандарту, символ ß (эсцет) эквивалентен ss, а умляут эквивалентен букве е (то есть ö — то же самое, что ое и т.д.).

Наш метод трансформации должен учитывать эти детали. Снова начнем с декомпозиции составных символов. Например, модифицирующая трема (умляут) представляется кодовой позициейU+0308. За основу мы возьмем метод преобразования регистра, имеющийся в Ruby, но несколько дополним его. Вот как выглядит теперь код трансформации:

def transform_de(str)

decomposed = Unicode.normalize_KD(str).downcase

decomposed.gsub!('ß', 'ss')

decomposed.gsub([0x0308].pack('U'), 'e')

end

 

array = ["Straße", "öffnen"]

array.map {|x| transform_de(x) } # ["strasse", "oeffnen"]

He для всех языков годится такой прямолинейный подход. Например, в испанском между буквами n и о есть еще буква ñ. Однако, если каким-то образом сдвинуть оставшиеся буквы, то мы справимся и с этой проблемой. В листинге 4.1 для упрощения обработки нормализация применена к монолитным символам. Кроме того, мы облегчили себе жизнь, игнорируя различия между буквами с диакритическими знаками и без них.





sdamzavas.net - 2019 год. Все права принадлежат их авторам! В случае нарушение авторского права, обращайтесь по форме обратной связи...