Когда я впервые узнал о кодировке 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. alan008
              21.09.2025 09:37

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

              PS Ни один язык полноценно все извраты юникода не поддерживает.

              Почитайте тут, например:

              https://tonsky.me/blog/unicode/


              1. altaastro
                21.09.2025 09:37

                Внутри России, внезапно, есть языки помимо русского, исәнмесез


              1. ccapt
                21.09.2025 09:37

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


          1. aeder
            21.09.2025 09:37

            К сожалению, кодируют. И Ё, и Й кодируют. Я сам нарывался - при копировани из MS Excel названия документа - скопировалость именно таким образом.

            Не представляете, как потом бесило то, что файл не мог быть найден в файловой системе - хотя вот он, блин, лежит.

            Но это не проблема UTF-8 - это Unicode в целом.


      1. neugomonnik
        21.09.2025 09:37

        Так вроде просто же, нет? Считаем все байты, которые начинаются с 0 или 11. Все байты 10 отбрасываем, т.к. они не добавляют длины


        1. Alex_v99
          21.09.2025 09:37

          Их для этого каждый парсить надо, а в ASCII просто взять File.Length и всё.


          1. stan_volodarsky
            21.09.2025 09:37

            Если вас переводы строки не интересуют, то да. А так они бывают по одному символу и по два.


    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. navferty
          21.09.2025 09:37

          Не 13, а 11 же?

           11110xxx 10xxxxxx 10xxxxxx 10xxxxxx


          1. Hlad
            21.09.2025 09:37

            Точно, обсчитался.


        1. VADemon
          21.09.2025 09:37

          cc: @edogshttps://utf8everywhere.org/ Там приведены убедительные аргументы за.


        1. edo1h
          21.09.2025 09:37

          средняя длина текста практически не изменилась бы

          вы сейчас про какой язык?


          1. Hlad
            21.09.2025 09:37

            Про любой, использующий не латинский алфавит.


        1. ccapt
          21.09.2025 09:37

          какие проблемы - есть utf-32. но все почему-то пользуются utf-8 (и немного - азиатским гибридом utf-16). казалось бы, почему. диктат теневого правительства?


    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. vanxant
          21.09.2025 09:37

          да, и заморозился в спецификациях строк, например, в JS. Т.е. str.length() возвращает длину строки именно в двухбайтовых кодпоинтах. Всё что туда не влезло, например, иппонские иероглифы или смайлики, заниманиют несколько кодовых поинтов и соответственно str.length() для них возращает хз что.


    1. afterone
      21.09.2025 09:37

      BOM это боль. Особенно когда какой-нибудь банальный python-скрипт или tex-файл не компилируются ровно из-за этого.

      А ещё спец-символы UTF-8 могут использоваться для стеганографии, например идентификации утечек текстов. Я тут недавно начал сначала замечать странные символы в статьях Хабра (только в Firefox, типа �, в рандомном месте статьи), а сейчас они пропали. Паранойя-паранойей, но почему бы и нет.


      1. vanxant
        21.09.2025 09:37

        Одних пробелов 11, кажется, штук - разной ширины. Плюс ещё \t, \r и \n. Учитывая, что html вообще воспринимает любое количество подряд идущих любых пробелов, кроме неразрывных, как один пробел, можно совершенно беспалевно вставлять в любой html документ или фрагмент (комментарий, например), любое невидимое сообщение.


      1. ImagineTables
        21.09.2025 09:37

        Паранойя-паранойей, но почему бы и нет.

        Как параноик параноика, хочу спросить: а вы заметили, что в книжках одного популярного в России ежегодного автора какие-то очень странные опечатки? Типа пропущенного пробела в определённых местах? Между тем, книжка изначально электронная, никто её не сканировал и не OCR'ил. Где-то пару десятков бит. То есть, как раз примерно соответствует продажам.

        Возможно, это сделано для того, чтобы отвлечь внимание от спецсимволов UTF-8! Люди исправят ошибки, и попадутся! (Это если не копать глубже…)


        1. afterone
          21.09.2025 09:37

          Нет, не заметил (не читал совсем, если быть точным). Идея интересная. Пробелы удобны тем, что их легко идентифицировать в производных версиях, типа бумажных или "распечатки в pdf". А UTF-8 наоборот, сохраняются в электронном виде. Кстати, одно другому не мешает! И согласен, это если не копать глубже )


      1. alex1478
        21.09.2025 09:37

        Firefox, типа �, в рандомном месте

        Уже недели две так


      1. spsettler
        21.09.2025 09:37

        И не только в Firefox: Яндекс, Edge


        1. afterone
          21.09.2025 09:37

          О! Тогда это действительно похоже на "кривую идентификацию". Я открывал одну и ту же статью в Firefox и Edge, в первом символы были, во втором нет. Для интереса нашёл статью со скриншота - у меня ни там, ни там спецзначков нет. В общем, это какая-то не очевидная штука, будем посмотреть.


    1. Mirn
      21.09.2025 09:37

      а какой "кайф" втискивать в микроконтроллеры поддержку UTF-8 для азаитских языков например для японского для поддержки топонимов и названий (а в топонимах и именах и тд японцы проявляют чудеса эрудиции и знаний нестандартных символов и крайне редких иероглифов). Я разработал хитрый алгоритм битовой компрессии карты символов 16х16 пикселей в всего лишь 350к и скорость трансляции сопоставимую с рендерингом классической ASCII. если таблица в 350к вся влезла в .rodata секцию. Для этого я выкачал и проанализировал все обезличеные реестры имён, ников, топонимов что были японцами опубликованы, "всего лишь" под 100 гигабайт. Ну и скриптами проанализировал воообще весь японоязычный домен википедии - там нашлось ещё несколько сотен нестандартных уникод-точек.