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]
Практические правила
- Везде UTF-8: файлы исходного кода, базы данных, HTTP-ответы (
Content-Type: text/html; charset=utf-8), терминал. - Декодировать на входе, кодировать на выходе: внутри программы работать со строками, а байты — только на границах (файлы, сеть).
- Указывать кодировку явно при открытии файлов:
open(path, encoding="utf-8")— иначе на Windows может использоваться системная кодировка. - Не резать строки по байтам: обрезка посередине многобайтного символа ломает текст.
- Не добавлять BOM (маркер порядка байтов) в UTF-8 без необходимости — он ломает скрипты с
#!и некоторые парсеры. - В PostgreSQL создавать базы с кодировкой UTF8, в MySQL использовать
utf8mb4, а не урезаннуюutf8, которая не вмещает эмодзи.
Подробное руководство по работе с Unicode в Python — в официальной документации.[6]