
Когда я впервые узнал о кодировке UTF-8, то был поражён её продуманностью и структурой. Тем, как изящно её авторам удалось выразить миллионы символов разных языков и письменностей, параллельно сохранив обратную совместимость с ASCII.
В UTF-8 используется 32 бита, а в старой доброй ASCII — 7 бит. Но UTF-8 выстроена так, чтобы:
Любой файл в кодировке ASCII являлся валидным файлом UTF-8.
Любой файл в кодировке UTF-8, имеющий только символы ASCII, также являлся валидным файлом ASCII.
Спроектировать систему, способную масштабироваться на миллионы символов и сохранить совместимость со старыми стандартами, использующими всего 128 символов — это гениально.
Примечание: когда я исследовал особенности UTF-8, то не нашёл ни одного хорошего инструмента, который бы позволял интерактивно визуализировать её работу. Поэтому я разработал собственную песочницу, которая открыта для ваших экспериментов.
Принцип работы UTF-8
UTF-8 — это кодировка символов с переменной шириной, созданная для выражения любого символа Юникода, включая знаки из большинства письменных систем мира.
Для кодирования символов в ней используется от одного до четырёх байтов.
Первые 128 символов (от U+0000
до U+007F
) кодируются с использованием одного байта, обеспечивая обратную совместимость с ASCII. Собственно, поэтому файл, содержащий только символы ASCII, является валидным файлом UTF-8.
Для представления других символов используется уже от двух до четырёх байтов. В каждом символе старшие биты первого байта определяют общее количество байтов. Для этого используется четыре разных паттерна.
Структура 1-го байта | Байтов используется | Структура всей последовательности байтов |
0xxxxxxx | 1 | 0xxxxxxx |
110xxxxx | 2 | 110xxxxx 10xxxxxx |
1110xxxx | 3 | 1110xxxx 10xxxxxx 10xxxxxx |
11110xxx | 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
Заметьте, что второй, третий и четвёртый байты в последовательности всегда начинаются с 10. Это говорит о том, что они являются байтами продолжения, следующими за основным.
Оставшиеся биты основного байта вместе с битами последующих байтов формируют кодовую точку символа. Кодовая точка выступает уникальным идентификатором символа в наборе Юникода. Обычно она выражается в шестнадцатеричной форме с префиксом «U+
». Например, для «А» кодовой точкой является U+0041
.
Теперь рассмотрим порядок действий, которому следует программное обеспечение для определения символа по байтам UTF-8:
Чтение байта. Если он начинается с 0, значит, перед нами однобайтовый символ (ASCII). В этом случае на экран выводится символ, представленный остальными 7 битами, и программа переключается на следующий байт.
Если первый байт начинался не с
0
, то:o если он начинается с
110
, значит, это двухбайтовый символ, и тогда считывается следующий байт,o если он начинается с
1110
, значит, символ трёхбайтовый, и тогда считываются два следующих байта,o если он начинается с
11110
, значит, это четырёхбайтовый символ, и тогда считываются три следующих байта.После выяснения количества байтов считываются все остальные биты, кроме старших и определяется двоичное значение (то есть кодовая точка) символа.
Программа ищет эту кодовую точку в списке Юникода и выводит соответствующий символ на экран.
Далее считывается следующий байт, и процесс повторяется.
Пример: буква «अ» из языка хинди
Буква «अ» (официально «буква А в письме деванагари») в формате UTF-8 выглядит так:
11100000 10100100 10000101
Первый байт 11100000
указывает, что символ закодирован с использованием трёх байтов.
Продолжающие биты этих байтов — xxxx0000 xx100100 xx000101
— совмещаются, формируя двоичную последовательность 00001001 00000101
(0x0905
в шестнадцатеричной форме). Это кодовая точка U+0905
.
U+0905
в наборе символов Юникода представляет букву «अ» из языка хинди (официальная спецификация).
Пример текстовых файлов
Теперь, когда мы разобрались в структуре UTF-8, разберём файл со следующим текстом:
1. Текст файла: Hey👋 Buddy
В выражении «Hey👋 Buddy» присутствуют английские символы и эмодзи. Если сохранить файл с этим текстом на диск, то он будет содержать 13 байт:
01001000 01100101 01111001 11110000 10011111 10010001 10001011 00100000 01000010 01110101 01100100 01100100 01111001
Разберём этот файл по байтам, следуя правилам декодирования UTF-8:
Байт | Трактовка |
| Начинается с |
| Начинается с |
| Начинается с |
| Начинается с |
| Начинается с |
| Начинается с |
| Начинается с |
| Начинается с |
| Начинается с |
| Начинается с |
| Начинается с |
| Та же 'd'. |
| Начинается с |
Теперь это валидный файл UTF-8, но он не обязательно должен быть «обратно совместимым» с ASCII, так как содержит и чуждый для этой кодировки символ (эмодзи). Теперь создадим файл, который будет содержать только символы ASCII.
2. Текст файла: Hey Buddy
В этом файле присутствуют только символы ASCII, и после сохранения на диск мы найдём в нём следующие 9 байт:
01001000 01100101 01111001 00100000 01000010 01110101 01100100 01100100 01111001
Разберём их аналогичным образом:
Байт | Трактовка |
| Начинается с |
| Начинается с |
| Начинается с |
| Начинается с |
| Начинается с |
| Начинается с |
| Начинается с |
| Та же 'd'. |
| Начинается с |
Итак, здесь у нас валидный файл UTF-8, который также является валидным файлом ASCII, поскольку содержащиеся в нём байты соответствуют правилам обеих этих кодировок.
Другие кодировки
Я поискал в сети информацию о других кодировках, которые тоже являются обратно совместимыми с ASCII. Удалось найти несколько, но они не так популярны, как UTF-8. Один из примеров — это GB 18030 (стандарт, используемый правительством Китая). Или однобайтовые кодировки ISO/IEC 8859, расширяющие ASCII дополнительными символами — правда, символов в них всего 256.
Родственники UTF-8 — UTF-16 и UTF-32 — уже не имеют обратной совместимости с ASCII. Например, буква 'A' в UTF-16 представлена как: 00 41
(два байта), а в UTF-32 она выражается уже четырьмя байтами: 00 00 00 41
.