Резюме: некоторые символы (воспринимаемые пользователями) представлены несколькими символами Unicode. Программисты должны знать об этом при измерении длины текста или разделении текста. В этой статье описаны такие случаи и представлена ​​библиотека Python для обработки таких кластеров символов.

Изучение эликсира или что такое графема?

У меня есть опыт работы с Python и JS. Как и большинство программистов, я пишу код, который время от времени обрабатывает текст. Я привык писать len(string) или string.length, чтобы получить количество символов в строке, и делаю это без особых размышлений.

Когда я начал работать с Эликсиром, я читал о стандартном модуле String. В этой документации была представлена ​​концепция графем, что, по-видимому, означало, что некоторые из моих представлений о тексте Unicode были неправильными.

Согласно глоссарию Unicode, графема - это:

(1) Минимально отличительная единица письма в контексте конкретной системы письма. Например, ‹b› и ‹d› - разные графемы в английских системах письма, потому что существуют разные слова, такие как big и dig. И наоборот, строчная курсивная буква a и строчная римская буква a не являются отдельными графемами, потому что ни одно слово не различается на основе этих двух разных форм. (2) Что пользователь думает о персонаже.

Для традиционного текста ASCII (без окончания строки CRLF) все графемы представлены одной кодовой точкой Unicode, но это не всегда так. Некоторые графемы представлены последовательностью из двух или более символов Юникода (кодовых точек).

Стандартные правила для графем определены в Приложении № 29 к стандарту Unicode.

Примеры графем

Это некоторые из типов графем с несколькими кодовыми точками, которые могут существовать. Обратите внимание, что все примеры кода Python находятся в Python 3, поэтому строки по умолчанию являются строками Unicode (байтовые строки - это совсем другое зверюга).

Диакритические знаки

К некоторым символам добавлены диакритические знаки, например å, ä и ö (используется на моем родном языке, шведском). Некоторые общие символы с диакритическими знаками имеют один символ Unicode (включая å, ä и ö), но они также могут быть представлены базовым символом, за которым следует символ Unicode для диакритического знака: `

>>> "a\u0308"
'ä'

Несмотря на то, что "ä" и "a\u0308" представляют один и тот же символ / графему, они не равны в Python или большинстве других языков. И у них разная длина. Это можно сделать с помощью unicodedata.normalize, но это можно использовать только для нескольких диакритических комбинаций, которые имеют обозначенный символ Unicode.

Некоторые символы с диакритическими знаками не имеют назначенного символа Unicode. Один из примеров - специальные символы, используемые в эсперанто: ĉ, ĝ, ĥ, ĵ, ŝ и ŭ. ĉ представлен как U + 0065 (латинская строчная буква e) + U + 005E (Circumflex Accent).

>>> len("ĝi")
3

корейский язык

Корейский алфавит хангыль состоит из 40 букв. Однако они не пишутся последовательно, как в латинском алфавите, а сгруппированы в блоки, образующие один слог. Я не знаю корейского, но приведу пример из статьи в Википедии:

То есть, хотя слог 한 han может выглядеть как один символ, на самом деле он состоит из трех букв: ㅎ h, ㅏ a, и ㄴ n.

>>> len("한")
3

Модификаторы эмодзи

Некоторые смайлы поддерживают модификаторы, изменяющие их внешний вид. Наиболее распространенными, вероятно, являются модификаторы тона кожи. Например, U + 270A (поднятый кулак) отображается как ✊. Но если следующий символ - 🏾 (U + 1F3FE - модификатор эмодзи Фитцпатрик, тип 5), он отображается как ✊🏾.

Это, вероятно, самая распространенная проблема, с которой могут столкнуться современные веб-разработчики, если не учитывать графемы. Как получить первый символ в строке ”✊🏾 some text” в Python?

>>> "✊🏾 some text"[0]
'✊'
>>> "✊🏾 some text"[1:]
'🏾 some text'

Эмодзи-флаги

Национальные флаги смайликов представлены в виде последовательности кодовых точек, соответствующих двухбуквенному коду страны. Таким образом, 🇸🇪 (Швеция / SE) представлен двумя символами Юникода: U + 1F1F8 (символ регионального индикатора, буква S) + U + 1F1EA (региональный индикаторный символ, буква E):

>>> "Welcome to Sweden: 🇸🇪"[-1]
'🇪'

Другое странное дерьмо

Сегодня в Стокгольме день гордости, поэтому нет лучшего дня для изучения текстового представления 🏳️‍🌈. Этот простой и красивый флаг на самом деле представлен четырьмя символами Unicode:

  • 🏳 U+ 1F3F3 (Белый флаг). Белый флаг развевается на столбике. Этот флаг, который традиционно используется как знак капитуляции, менее определен как смайлик.
  • ️ U + FE0F (Селектор вариации-16). Невидимая кодовая точка, которая указывает, что предыдущий символ должен отображаться с представлением эмодзи. Требуется только в том случае, если предыдущий символ по умолчанию используется для текстового представления.
  • ‍ U + 200D (Столяр нулевой ширины). Объединитель нулевой ширины (ZWJ) - это символ Unicode, который последовательно объединяет два или более других символа вместе для создания нового эмодзи.
  • 🌈 U + 1F308 (Радуга)
>>> len("🏳️‍🌈")
4

Python vs Elixir - что такое персонаж?

(Современный) Python представляет строки как последовательность кодовых точек Unicode. Все строковые операции, связанные с подсчетом символов, основаны на кодовых точках без учета графем. Таким образом, len(string) вернет количество кодовых точек Unicode в строке, а при нарезке и индексировании будет использоваться последовательность кодовых точек, а не последовательность графем.

Модуль String в Elixir по умолчанию учитывает графемы. String.length(string) подсчитает количество графем, а String.slice(string, 0, 10) вернет первые 10 графем (даже если для представления этих графем используется более 10 кодовых точек Unicode).

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

Знакомство с библиотекой Python grapheme

Читая об этом, я не мог найти простой в использовании библиотеки Python для строковых операций с учетом графем. Поэтому я написал графему, которая содержит ряд функций для подсчета и нарезки на основе границ символов графемы.

>>> import grapheme
>>> grapheme.length("a\u0308")
1
>>> grapheme.length("ĝi")
2
>>> grapheme.length("한")
1
>>> grapheme.slice("✊🏾 some text", 0, 1)
'✊🏾'
>>> grapheme.slice("✊🏾 some text", 1)
' some text'
>>> list(grapheme.graphemes("Welcome to Sweden: 🇸🇪"))[-1]
'🇸🇪'
>>> grapheme.length("🏳️‍🌈")
1

Пожалуйста, проверьте это на PyPi или GitHub.

Связанных с работой

Еще есть uniseg, очень похожего по назначению. Однако он все еще использует Unicode версии 6.2.0, поэтому в нем отсутствуют многие недавние изменения правил и новые добавленные символы. Текущая версия Unicode - 10.0.0.

Следует также рассмотреть PyICU, который представляет собой оболочку Python для библиотеки C ICU.