Когда я впервые узнал о кодировке 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
(По сути, это стандартный байт кодировки ASCII)

110xxxxx

2

110xxxxx 10xxxxxx

1110xxxx

3

1110xxxx 10xxxxxx 10xxxxxx

11110xxx

4

11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

Заметьте, что второй, третий и четвёртый байты в последовательности всегда начинаются с 10. Это говорит о том, что они являются байтами продолжения, следующими за основным.

Оставшиеся биты основного байта вместе с битами последующих байтов формируют кодовую точку символа. Кодовая точка выступает уникальным идентификатором символа в наборе Юникода. Обычно она выражается в шестнадцатеричной форме с префиксом «U+». Например, для «А» кодовой точкой является U+0041.

Теперь рассмотрим порядок действий, которому следует программное обеспечение для определения символа по байтам UTF-8:

  1. Чтение байта. Если он начинается с 0, значит, перед нами однобайтовый символ (ASCII). В этом случае на экран выводится символ, представленный остальными 7 битами, и программа переключается на следующий байт.

  2. Если первый байт начинался не с 0, то:

    o    если он начинается с 110, значит, это двухбайтовый символ, и тогда считывается следующий байт,

    o    если он начинается с 1110, значит, символ трёхбайтовый, и тогда считываются два следующих байта,

    o    если он начинается с 11110, значит, это четырёхбайтовый символ, и тогда считываются три следующих байта.

  3. После выяснения количества байтов считываются все остальные биты, кроме старших и определяется двоичное значение (то есть кодовая точка) символа.

  4. Программа ищет эту кодовую точку в списке Юникода и выводит соответствующий символ на экран.

  5. Далее считывается следующий байт, и процесс повторяется.

Пример: буква «अ» из языка хинди

Буква «अ» (официально «буква А в письме деванагари») в формате 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:

Байт

Трактовка

01001000

Начинается с 0, значит, это однобайтовый символ ASCII. Оставшиеся биты 1001000 представляют букву 'H' (открыть в песочнице).

01100101

Начинается с 0, значит, это также однобайтовый символ ASCII. Его оставшиеся биты 1100101 представляют букву 'e' (открыть в песочнице).

01111001

Начинается с 0, значит, и это однобайтовый символ ASCII. Его оставшиеся биты 1111001 означают букву 'y' (открыть в песочнице).

11110000

Начинается с 11110, значит, это первый байт четырёхбайтового символа.

10011111

Начинается с 10, то есть это байт продолжения.

10010001

Начинается с 10 — тоже байт продолжения.

10001011

Начинается с 10 — последний байт продолжения.

Оставшиеся биты этих четырёх байтов (то есть кроме старших) формируют двоичную последовательность 00001 11110100 01001011, которая в шестнадцатеричном виде представляет 1F44B и соответствует кодовой точке U+1F44B. В наборе Юникода эта точка выражает эмодзи машущей руки «?» (открыть в песочнице).

00100000

Начинается с 0, то есть относится к однобайтовому символу ASCII. Оставшиеся биты 0100000 представляют символ пробела (открыть в песочнице).

01000010

Начинается с 0 — снова однобайтовый символ ASCII. Оставшиеся биты 1000010 представляют букву 'B' (открыть в песочнице).

01110101

Начинается с 0 — однобайтовый символ. Оставшиеся биты 1110101 представляют букву 'u' (открыть в песочнице).

01100100

Начинается с 0 — однобайтовый символ. Оставшиеся биты 1100100 представляют букву 'd' (открыть в песочнице).

01100100

Та же 'd'.

01111001

Начинается с 0 — однобайтовый символ ASCII. Оставшиеся биты 1111001 представляют букву 'y' (открыть в песочнице).

Теперь это валидный файл UTF-8, но он не обязательно должен быть «обратно совместимым» с ASCII, так как содержит и чуждый для этой кодировки символ (эмодзи). Теперь создадим файл, который будет содержать только символы ASCII.

2. Текст файла: Hey Buddy

В этом файле присутствуют только символы ASCII, и после сохранения на диск мы найдём в нём следующие 9 байт:

01001000 01100101 01111001 00100000 01000010 01110101 01100100 01100100 01111001

Разберём их аналогичным образом:

Байт

Трактовка

01001000

Начинается с 0 — однобайтовый символ ASCII. Оставшиеся биты 1001000 представляют букву 'H' (открыть в песочнице).

01100101

Начинается с 0 — однобайтовый символ ASCII. Оставшиеся биты 1100101 представляют букву 'e' (открыть в песочнице).

01111001

Начинается с 0 — однобайтовый символ ASCII. Оставшиеся биты 1111001 представляют букву 'y' (открыть в песочнице).

00100000

Начинается с 0 — однобайтовый символ ASCII. Оставшиеся биты 0100000 представляют символ пробела (открыть в песочнице).

01000010

Начинается с 0 — однобайтовый символ ASCII. Оставшиеся биты 1000010 представляют букву 'B' (открыть в песочнице).

01110101

Начинается с 0 — однобайтовый символ ASCII. Оставшиеся биты 1110101 представляют букву 'u' (открыть в песочнице).

01100100

Начинается с 0 — однобайтовый символ ASCII. Оставшиеся биты 1100100 представляют букву 'd' (открыть в песочнице).

01100100

Та же 'd'.

01111001

Начинается с 0 — однобайтовый символ ASCII. Оставшиеся биты 1111001 представляют букву 'y' (открыть в песочнице).

Итак, здесь у нас валидный файл 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)


  1. edogs
    21.09.2025 09:37

    • Любой файл в кодировке UTF-8, имеющий только символы ASCII, также являлся валидным файлом ASCII.

    Как же нас бомбит от BOM-а в начале обычных текстовых файлов:) Особенно на фоне того, что некоторые редакторы его включают по умолчанию, а некоторые нет. Даже больше чем \r\n vs \n vs \r в разных системах.

    UTF-8 — это кодировка символов с переменной шириной, созданная для

    Невозможности подсчета количества букв, просто по размеру файла даже в простом варианте (ну тут понятно о чем речь).
    Для невозможности анализа нужного количества символов на экране для вывода текста, без анализа логики всего текста (символы назад-вперед-вверх и т.д.).
    Для увеличения объема памяти для хранения даже простого текста, что сказывается и на скорости обработки (до 2 раз легко, наша старая статья тут же на хабре про utf8 и cp1251 до сих пор актуальна минимум на 3/4, один лишь переход на cp1251 на русскоязычных текстах делает работу прог заметно более шустрой и менее энергоемкой)
    Для создания неоднозначностей и уязвимостей через них (внешне одно и то же может быть по разному кодировано. I и l отдыхают).
    Для возможности создать текст с недопустимыми последовательностями (особенно прелестно когда это вылезает при обрезании строки, для грамотного обрезания которой нужно анализировать логику последовательности символов с самого первого и до самого последнего, а не просто резануть где надо).
    Во избежании однозначности при обработке текста (разные либы и разные проги очень по разному обрабатывают utf-8).
    И так далее.
    Анноит нас utf-8, вот прям подгорает.
    Но да, сейчас нас за эту позицию будут бить, возможно даже ногами:(


    1. ReadOnlySadUser
      21.09.2025 09:37

      Да почему) Каждый, кто хоть раз попытался посчитать длину UTF-8 строки задумывался над тем, что "как-то сложновато")


      1. aamonster
        21.09.2025 09:37

        В плане подсчёта длины напрягает не столько utf-8, сколько unicode в целом с его символами из нескольких codepoints.


        1. alan008
          21.09.2025 09:37

          Символы из нескольких кодовых точек (суррогатные пары) в русском/английском практически не встречаются и не используются. В смысле что никто не будет кодировать ё как е+символ с двумя точками над буквой, это не удобно и не практично. В каких-то сложных языках (типа индийского), возможно, это применяется чаще.


          1. MountainGoat
            21.09.2025 09:37

            И что? Часто вы можете сказать, что ваша программа будет обрабатывать только тексты на английском и русском, а все остальные идут нахрен? Ну тогда сразу и используйте только cp1251, чтобы впоследствии не было непонимания.


    1. DjUmnik
      21.09.2025 09:37

      Кто ж виноват, что ASCII стал стандартом на компьютерах. Кто первый встал, того и тапки!
      Теперь мы расплачиваемся раздутыми русскими текстами. Но кого это волнует сейчас, в наш век быстрых процессоров, гигабитного интернета и терабайтных носителей? Или нейросети будут потреблять меньше электричества, если их кормить в 8-битной кодировке?


      1. ReadOnlySadUser
        21.09.2025 09:37

        Вообще, скорее всего будут) Но количество электроэнергии необходимого для конвертации всего и вся в 8 битную кодировку скорее всего сопоставимо с выигрышем, если не вообще есть.


      1. Hlad
        21.09.2025 09:37

        Речь о том, что если бы авторы стандарта не выпендривались, а тупо забубенили чистую трёхбайтную кодировку, то символов бы кодировалось столько же, средняя длина текста практически не изменилась бы, а жить было бы намного проще. В четырёх байтах UTF8 13 служебных бит, эффективная глубина кодирования - меньше 2.5 байт.


    1. ImagineTables
      21.09.2025 09:37

      UTF-8 — это кодировка символов с переменной шириной

      По-моему, это просто неправда.

      Кодировка символов с переменной шириной это Юникод. А UTF-8 — кодировка кодепоинтов с переменной шириной. Поэтому текст в UTF-8 имеет дважды переменную ширину, сначала кодепоинтов, потом символов.

      Весь ужас Юникода именно в том, что ни одна его кодировка не позволит имея указатель и размер буфера вычислить длину строки в символах без перебора. Даже UTF-32.


      1. ImagineTables
        21.09.2025 09:37

        ни одна его кодировка

        Поправка: ни одна актуальная. Плоский (fixed-width) UCS-2 когда-то назывался Юникодом.


  1. Sazonov
    21.09.2025 09:37

    Вот тут получше разжёвано про преимущества и недостатки: https://utf8everywhere.org


    1. MountainGoat
      21.09.2025 09:37

      То, что переводы предлагаются только на китайский и иврит, тоже говорит о чём-то.


  1. lealxe
    21.09.2025 09:37

    Я не хочу никого обидеть, но понятие кодового дерева, хотя и прекрасно, появилось за-а-а-адо-о-олго до UTF-8, и ничего особенного в его применении нет.

    Зато посчитать без споров количество символов и ширину текста для этой замечательной кодировки очень трудно.

    А еще в ней латинский европейский текст весит меньше, чем, скажем, кириллический, и совсем меньше, чем японский или китайский. Это не очень хорошо.

    Когда-то был фанатом ее, но если честно - я за управляющие последовательности для переключения разных кодировок отображения, или за указание их в метаданных хотя бы.

    Или, если хочется весь юникод в одну кодировку, UCS-32 по 4 байта на символ.

    А для сжатия существует deflate.


    1. ImagineTables
      21.09.2025 09:37

      Когда-то был фанатом ее, но если честно - я за управляющие последовательности для переключения разных кодировок отображения, или за указание их в метаданных хотя бы.

      И как в одной строке хранить текст на разных языках? Допустим, слоган со словом «Мир» на всех. Что должно быть в метаданных? Пары «оффсет-кодепейдж»? ))

      Или, если хочется весь юникод в одну кодировку, UCS-32 по 4 байта на символ.

      Очень бы хотелось видеть как альтернативу Юникоду. Именно 4 байта на символ, fixed-width.


      1. XViivi
        21.09.2025 09:37

        UTF-32 разве как раз этого не делает?


      1. MountainGoat
        21.09.2025 09:37

        И как в одной строке хранить текст на разных языках? 

        Служебный символ, означающий "дальше переключаемся на страницу такую-то". Да, строка, содержащая по букве из каждого языка, будет сильно раздута. Но всё ещё возможна. А это очень редкое применение. Зато текст на сплошном менгрельском не будет иметь по 4 байта на букву.


  1. sheshanaag
    21.09.2025 09:37

    Алгоритм utf8 красивый, но его совместимость с ascii была бы невозможна, если бы в ascii не зарезервировали бы бит, который используется для кодирования не-ascii символов unicode, так что разработчики ascii достойны не меньшей похвалы.


  1. OlegZH
    21.09.2025 09:37

    По своей сути, Unicode — это большая таблица современного знакогенератора. Но проблема кодирования символов так и осталось неразрешённой. Или, всё-таки, неразрешимой?