Unicode и UTF-8

Материал из llmdocs — справочник разработчика
Портал: Основы CS
Unicode и UTF-8
Unicodeстандарт кодирования символов, Консорциум Юникода (с 1991)
Кодовое пространствоU+0000 — U+10FFFF (1 114 112 кодовых точек)
Число символовболее 150 тысяч
КодировкиUTF-8, UTF-16, UTF-32
UTF-8Кен Томпсон и Роб Пайк (1992), RFC 3629
Доля веб-сайтов в UTF-8около 98%
Совместимостьпервые 128 символов совпадают с ASCII

Unicode (Юникод) — международный стандарт, который присваивает уникальный номер — кодовую точку — каждому символу всех письменностей мира: латинице, кириллице, иероглифам, арабскому письму, математическим знакам, эмодзи. UTF-8 — самая распространённая кодировка Unicode, то есть способ записать эти номера байтами: от одного байта для латиницы до четырёх для эмодзи. Сегодня в UTF-8 работает около 98% веб-сайтов,[7] и это кодировка по умолчанию в Linux, Python, Go, Rust, JSON и HTML.

Unicode решил проблему, мучившую программистов десятилетиями: текст, набранный на одном компьютере, превращался в «кракозябры» на другом.

История

Первые кодировки, например ASCII (1963), описывали 128 символов — латиницу, цифры и знаки препинания. Для других языков создавали 8-битные расширения, и для одного только русского их было несколько: КОИ-8, CP866 (DOS), Windows-1251, ISO 8859-5. Текст в одной кодировке, прочитанный в другой, превращался в бессмыслицу вроде «Ïðèâåò» — по-японски это называют mojibake.

В 1991 году Консорциум Юникода выпустил версию 1.0 стандарта с единым набором символов для всех языков.[1] Сначала предполагалось, что хватит 65 536 символов и 16 бит на каждый, но вскоре пространство расширили до 1 114 112 кодовых точек. Новые версии выходят ежегодно и добавляют письменности и эмодзи; сейчас в стандарте более 150 тысяч символов.

В сентябре 1992 года Кен Томпсон и Роб Пайк придумали UTF-8 — по легенде, за один вечер в закусочной в Нью-Джерси — и в течение недели перевели на неё операционную систему Plan 9.[5] Стандарт RFC 3629 (2003) ограничил UTF-8 четырьмя байтами на символ.[2]

Кодовые точки и кодировки

Кодовая точка записывается как U+ и шестнадцатеричный номер: U+0041 — латинская «A», U+0416 — «Ж», U+1F600 — 😀. Unicode определяет символы и их свойства, но не то, как хранить номера в байтах, — это делают кодировки:

Кодировка Байтов на символ Особенности
UTF-8 1–4 совместима с ASCII, нет проблем с порядком байтов; стандарт веба и Unix
UTF-16 2 или 4 внутреннее представление строк в Windows, Java, JavaScript
UTF-32 4 фиксированная длина, расточительна по памяти

Как устроена UTF-8

Диапазон Байты Шаблон
U+0000 — U+007F 1 0xxxxxxx — ASCII без изменений
U+0080 — U+07FF 2 110xxxxx 10xxxxxx — кириллица, греческий, иврит
U+0800 — U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx — большинство иероглифов
U+10000 — U+10FFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx — эмодзи, редкие символы

Схема удачна сразу в нескольких отношениях: любой ASCII-текст уже является корректной UTF-8, по первому байту видно длину символа, а с любого места потока можно найти начало следующего символа. Русская буква в UTF-8 занимает два байта, поэтому русский текст вдвое «тяжелее» английского по байтам — это же влияет и на токенизацию в языковых моделях.

s = "Привет, мир! 👋"
print(len(s))                    # 14 — кодовых точек
print(len(s.encode("utf-8")))    # 26 — байтов: кириллица по 2, эмодзи 4
print(s.encode("utf-8")[:4])     # b'\xd0\x9f\xd1\x80' — «Пр»
print(hex(ord("Ж")), chr(0x1F600))

Что считать символом

То, что пользователь видит как один символ, может состоять из нескольких кодовых точек:

  • «й» можно записать одной точкой U+0439 или двумя: «и» + комбинируемая кратка U+0306. Выглядят одинаково, но строки не равны.
  • Эмодзи семьи 👨‍👩‍👧 — несколько эмодзи, соединённых невидимым символом.
  • Флаги стран — пары специальных символов-букв.

Поэтому длина строки в кодовых точках не совпадает с числом видимых символов — графемных кластеров, правила разбиения на которые описаны в приложении UAX #29.[4] Для корректного сравнения строки нормализуют к единой форме (NFC — составные символы, NFD — разложенные), иначе поиск и сравнение имён файлов дают неожиданные результаты.[3]

Практические правила

  1. Везде UTF-8: файлы исходного кода, базы данных, HTTP-ответы (Content-Type: text/html; charset=utf-8), терминал.
  2. Декодировать на входе, кодировать на выходе: внутри программы работать со строками, а байты — только на границах (файлы, сеть).
  3. Указывать кодировку явно при открытии файлов: open(path, encoding="utf-8") — иначе на Windows может использоваться системная кодировка.
  4. Не резать строки по байтам: обрезка посередине многобайтного символа ломает текст.
  5. Не добавлять BOM (маркер порядка байтов) в UTF-8 без необходимости — он ломает скрипты с #! и некоторые парсеры.
  6. В PostgreSQL создавать базы с кодировкой UTF8, в MySQL использовать utf8mb4, а не урезанную utf8, которая не вмещает эмодзи.

Подробное руководство по работе с Unicode в Python — в официальной документации.[6]

См. также

Источники

  1. The Unicode Standard
  2. RFC 3629 — UTF-8, a transformation format of ISO 10646
  3. Unicode Standard Annex #15 — Unicode Normalization Forms
  4. Unicode Standard Annex #29 — Unicode Text Segmentation
  5. Rob Pike — UTF-8 history
  6. Python — Unicode HOWTO
  7. W3Techs — Usage statistics of character encodings for websites