
Когда я впервые узнал о кодировке 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
.
Комментарии (0)
Sazonov
21.09.2025 09:37Вот тут получше разжёвано про преимущества и недостатки: https://utf8everywhere.org
lealxe
21.09.2025 09:37Я не хочу никого обидеть, но понятие кодового дерева, хотя и прекрасно, появилось за-а-а-адо-о-олго до UTF-8, и ничего особенного в его применении нет.
Зато посчитать без споров количество символов и ширину текста для этой замечательной кодировки очень трудно.
А еще в ней латинский европейский текст весит меньше, чем, скажем, кириллический, и совсем меньше, чем японский или китайский. Это не очень хорошо.
Когда-то был фанатом ее, но если честно - я за управляющие последовательности для переключения разных кодировок отображения, или за указание их в метаданных хотя бы.
Или, если хочется весь юникод в одну кодировку, UCS-32 по 4 байта на символ.
А для сжатия существует deflate.
sheshanaag
21.09.2025 09:37Алгоритм utf8 красивый, но его совместимость с ascii была бы невозможна, если бы в ascii не зарезервировали бы бит, который используется для кодирования не-ascii символов unicode, так что разработчики ascii достойны не меньшей похвалы.
OlegZH
21.09.2025 09:37По своей сути, Unicode — это большая таблица современного знакогенератора. Но проблема кодирования символов так и осталось неразрешённой. Или, всё-таки, неразрешимой?
edogs
Как же нас бомбит от BOM-а в начале обычных текстовых файлов:) Особенно на фоне того, что некоторые редакторы его включают по умолчанию, а некоторые нет. Даже больше чем \r\n vs \n vs \r в разных системах.
Невозможности подсчета количества букв, просто по размеру файла даже в простом варианте (ну тут понятно о чем речь).
Для невозможности анализа нужного количества символов на экране для вывода текста, без анализа логики всего текста (символы назад-вперед-вверх и т.д.).
Для увеличения объема памяти для хранения даже простого текста, что сказывается и на скорости обработки (до 2 раз легко, наша старая статья тут же на хабре про utf8 и cp1251 до сих пор актуальна минимум на 3/4, один лишь переход на cp1251 на русскоязычных текстах делает работу прог заметно более шустрой и менее энергоемкой)
Для создания неоднозначностей и уязвимостей через них (внешне одно и то же может быть по разному кодировано. I и l отдыхают).
Для возможности создать текст с недопустимыми последовательностями (особенно прелестно когда это вылезает при обрезании строки, для грамотного обрезания которой нужно анализировать логику последовательности символов с самого первого и до самого последнего, а не просто резануть где надо).
Во избежании однозначности при обработке текста (разные либы и разные проги очень по разному обрабатывают utf-8).
И так далее.
Анноит нас utf-8, вот прям подгорает.
Но да, сейчас нас за эту позицию будут бить, возможно даже ногами:(
ReadOnlySadUser
Да почему) Каждый, кто хоть раз попытался посчитать длину UTF-8 строки задумывался над тем, что "как-то сложновато")
aamonster
В плане подсчёта длины напрягает не столько utf-8, сколько unicode в целом с его символами из нескольких codepoints.
alan008
Символы из нескольких кодовых точек (суррогатные пары) в русском/английском практически не встречаются и не используются. В смысле что никто не будет кодировать ё как е+символ с двумя точками над буквой, это не удобно и не практично. В каких-то сложных языках (типа индийского), возможно, это применяется чаще.
DjUmnik
Кто ж виноват, что ASCII стал стандартом на компьютерах. Кто первый встал, того и тапки!
Теперь мы расплачиваемся раздутыми русскими текстами. Но кого это волнует сейчас, в наш век быстрых процессоров, гигабитного интернета и терабайтных носителей? Или нейросети будут потреблять меньше электричества, если их кормить в 8-битной кодировке?
ReadOnlySadUser
Вообще, скорее всего будут) Но количество электроэнергии необходимого для конвертации всего и вся в 8 битную кодировку скорее всего сопоставимо с выигрышем, если не вообще есть.
Hlad
Речь о том, что если бы авторы стандарта не выпендривались, а тупо забубенили чистую трёхбайтную кодировку, то символов бы кодировалось столько же, средняя длина текста практически не изменилась бы, а жить было бы намного проще. В четырёх байтах UTF8 13 служебных бит, эффективная глубина кодирования - меньше 2.5 байт.
ImagineTables
По-моему, это просто неправда.
Кодировка символов с переменной шириной это Юникод. А UTF-8 — кодировка кодепоинтов с переменной шириной. Поэтому текст в UTF-8 имеет дважды переменную ширину, сначала кодепоинтов, потом символов.
Весь ужас Юникода именно в том, что ни одна его кодировка не позволит имея указатель и размер буфера вычислить длину строки в символах без перебора. Даже UTF-32.