Кодировки символов: какими бывают и чем отличаются

Кодировки символов: какими бывают и чем отличаются Полезное

Кодировка символов — это правило, которое ставит в соответствие символ (букву, цифру, знак) и его числовое представление, а затем последовательность байтов в памяти или файле. Разные кодировки бывают потому, что исторически по-разному отвечали на два вопроса: какие символы вообще существуют и сколько байтов тратить на каждый. В статье разберем главные семейства — ASCII, однобайтовые (Windows-1251, KOI8-R, ISO-8859), Unicode и его формы UTF-8, UTF-16, UTF-32, покажем на Python реальные байты для кириллицы и объясним, откуда берутся «кракозябры».

Два разных понятия: набор символов и кодировка

Здесь легко запутаться, поэтому разведем термины сразу и одной цепочкой.

Набор символов (character set) — это таблица «символ -> число». Число называется кодовой точкой (code point). Пример: в Unicode букве «Я» назначена кодовая точка U+042F.
Кодировка (encoding) — это правило «кодовая точка -> байты». Пример: UTF-8 записывает U+042F двумя байтами D0 AF.
Итого сквозная цепочка: символ «Я» -> кодовая точка U+042F -> байты D0 AF в UTF-8.

Кодовая точка — это абстрактное число, а байт — то, что реально лежит на диске. В старых однобайтовых кодировках эти уровни совпадали (одна точка = один байт), поэтому разницу часто не замечали. В Unicode они разошлись, и путаница «Unicode = UTF-8» родом именно отсюда.

ASCII: фундамент

ASCII — это набор из 128 символов (кодовые точки 0-127), который помещается в 7 бит. В него входят латинские буквы, цифры, знаки препинания и управляющие символы. Кириллицы, умляутов и эмодзи в нем нет.

ASCII важен потому, что его первые 128 позиций легли в основу почти всех последующих кодировок: и однобайтовых, и UTF-8. Если текст состоит только из латиницы и цифр, он выглядит одинаково почти везде — совместимость с ASCII сохраняется.

Однобайтовые кодировки: 256 символов и их беда

Когда потребовались национальные алфавиты, свободный восьмой бит использовали для второй половины таблицы (позиции 128-255). Так появились однобайтовые кодировки: первые 128 символов — как в ASCII, а верхние 128 у каждой свои.

Для кириллицы распространились несколько несовместимых вариантов:

  • Windows-1251 (cp1251) — основная кириллическая кодировка эпохи Windows.
  • KOI8-R — кодировка, популярная в UNIX и ранней рунет-почте; буквы расставлены так, что при потере восьмого бита текст оставался читаемым транслитом.
  • ISO-8859-5 — часть семейства ISO-8859, где ISO-8859-1 (Latin-1) покрывает Западную Европу, а -5 добавляет кириллицу.
  • CP866 (DOS) — кодировка русской MS-DOS.

Общая беда однобайтовых кодировок: в 256 позиций помещается лишь один-два алфавита. Смешать русский, греческий и, скажем, японский в одном файле нельзя, а один и тот же байт 0xD0 в разных кодировках означает разные буквы. Это прямая причина «кракозябр».

Unicode: один набор символов для всех языков

Unicode — это единый набор символов, который назначает уникальную кодовую точку каждому знаку всех письменностей мира, а также эмодзи и техническим символам. Запись кодовой точки — U+ и шестнадцатеричное число: U+0041 (A), U+042F (Я), U+1F600 (эмодзи).

Важно: Unicode — это НЕ формат байтов. Он отвечает на вопрос «какие символы существуют и какой номер у каждого», но не говорит, как их хранить. За хранение отвечают формы кодирования — UTF-8, UTF-16, UTF-32. Их и путают с самим Unicode.

Пространство Unicode рассчитано на 1 114 112 кодовых точек (U+0000 — U+10FFFF); на 2026 год назначена лишь часть из них, стандарт регулярно пополняется. Точное число присвоенных символов зависит от версии стандарта.

Формы кодирования Unicode: UTF-8, UTF-16, UTF-32

Одну и ту же кодовую точку можно записать в байты по-разному. Отсюда три формы.

Кодировка Байтов на символ Совместима с ASCII Где применяется
ASCII 1 (7 бит) сама и есть основа только латиница, цифры, управляющие
Windows-1251 1 да (0-127) legacy-файлы и БД с кириллицей
KOI8-R 1 да (0-127) старая почта, UNIX-тексты
ISO-8859-5 1 да (0-127) часть семейства ISO-8859
UTF-8 1-4 (переменная) да, латиница = 1 байт веб, файлы, API — формат по умолчанию
UTF-16 2 или 4 нет внутренняя строка в Java, C#, JavaScript, Windows API
UTF-32 4 (фиксированная) нет обработка, где нужен прямой доступ по индексу точки

Коротко про различие форм:

  • UTF-8 — переменная длина 1-4 байта. Латиница занимает 1 байт (как ASCII), кириллица — 2, многие иероглифы — 3, эмодзи — 4. Экономна для текстов с латиницей и совместима с ASCII, поэтому стала стандартом в вебе и файлах.
  • UTF-16 — 1 или 2 «кодовые единицы» по 2 байта. Символы вне базовой плоскости (эмодзи и часть иероглифов) кодируются суррогатной парой (4 байта). Здесь появляется зависимость от порядка байтов (endianness), который помечается BOM. Используется внутри строк Java, C# и JavaScript.
  • UTF-32 — всегда 4 байта на кодовую точку. Расточительна по памяти, зато номер символа лежит в байтах напрямую.

Кодовая точка vs кодовая единица vs графема. Кодовая точка — номер символа в Unicode (U+042F). Кодовая единица — минимальный блок конкретной формы (1 байт в UTF-8, 2 байта в UTF-16). Графема — то, что человек видит как один знак; иногда она собрана из нескольких кодовых точек (буква с диакритикой, составной эмодзи). Поэтому «число байтов», «число кодовых точек» и «число видимых символов» — три разные величины.

Практика на Python: смотрим реальные байты

Метод encode превращает строку (кодовые точки) в байты по выбранной кодировке, а decode делает обратное. Посмотрим байты кириллицы в UTF-8 — минимальный полный пример, который можно скопировать и запустить.

s = "Привет"
b = s.encode("utf-8")            # строка -> байты
print(" ".join(f"{x:02X}" for x in b))   # шестнадцатеричные байты
print("байтов:", len(b), "символов:", len(s))
print("Я ->", hex(ord("Я")))     # кодовая точка буквы Я
print(b.decode("utf-8"))          # байты -> строка обратно

Фактический вывод:

D0 9F D1 80 D0 B8 D0 B2 D0 B5 D1 82
байтов: 12 символов: 6
Я -> 0x42f
Привет

Видно главное: 6 символов кириллицы заняли 12 байтов — по 2 байта на букву в UTF-8, тогда как латинская буква заняла бы 1 байт. Кодовая точка «Я» равна U+042F, а в байтах UTF-8 это D0 AF (проверьте отдельным "Я".encode("utf-8")).

Кракозябры: причина и как воспроизвести

«Кракозябры» (mojibake) появляются, когда байты записаны в одной кодировке, а прочитаны как другая. Байты не «портятся» — их просто трактуют по чужой таблице. Покажем ошибку тройкой: неверное чтение -> результат -> исправление.

raw = "Привет".encode("utf-8")   # байты D0 9F ... в UTF-8

# 1) неверно: читаем UTF-8 байты как Windows-1251
print(raw.decode("cp1251"))

Фактический вывод (типичные кракозябры):

Привет

Более грубая ошибка — прочитать те же байты как ASCII, где старших кодов нет:

raw.decode("ascii")

Фактический вывод — исключение:

UnicodeDecodeError: 'ascii' codec can't decode byte 0xd0 in position 0: ordinal not in range(128)

Исправление одно и то же: читать теми же байтами и той же кодировкой, которой писали.

print(raw.decode("utf-8"))   # -> Привет

Практический вывод: при чтении файла, ответа API или строки из БД всегда знайте и указывайте кодировку явно. Большинство современных источников — UTF-8, но legacy-выгрузки нередко приходят в Windows-1251.

Выводы

  • Кодировка — это правило «символ -> число -> байты»; различать надо набор символов (таблица символ -> кодовая точка) и кодировку (кодовая точка -> байты).
  • ASCII (128 символов, 7 бит) — общая основа; однобайтовые Windows-1251, KOI8-R, ISO-8859 добавляют по одному алфавиту в позиции 128-255 и потому взаимно несовместимы.
  • Unicode — это набор кодовых точек для всех письменностей, а не формат байтов; байтами его записывают формы UTF-8, UTF-16, UTF-32.
  • UTF-8 переменной длины (латиница 1 байт, кириллица 2, эмодзи 4) и совместима с ASCII — поэтому стала стандартом; кириллица в UTF-8 занимает 2 байта на букву.
  • Кракозябры — это чтение байтов чужой кодировкой; лечится указанием правильной кодировки, а не «починкой» текста.

Где применяется / связь с практикой

Работа с кодировками — повседневная часть системного и низкоуровневого программирования: чтение файлов и сетевых буферов, обмен данными между сервисами, разбор протоколов, где важно точно знать, сколько байтов занимает символ и в каком порядке они идут. Ошибка на этом уровне превращается в кракозябры, битые логи и падения парсеров, поэтому понимание перехода «кодовая точка -> байты» ценят везде, где данные ходят в бинарном виде.

Освойте тему на практике

Глубже разобраться с представлением данных в памяти, буферами и работой с байтами на низком уровне помогает курс Системное программирование. Прежде чем записываться, можно бесплатно оценить формат и уровень на открытых уроках Otus — там разбирают практические задачи вживую.

FAQ

Чем UTF-8 отличается от Unicode?
Unicode назначает символам номера (кодовые точки), а UTF-8 — это одно из правил, как записать эти номера в байты. Unicode — набор символов, UTF-8 — форма кодирования.

Почему кириллица в UTF-8 занимает 2 байта, а латиница 1?
UTF-8 переменной длины: кодовые точки 0-127 (латиница, цифры) пишутся одним байтом ради совместимости с ASCII, а кириллица лежит выше и требует двух байтов.

Какую кодировку выбрать для нового файла или базы?
По умолчанию UTF-8: она совместима с ASCII, покрывает все языки и стала стандартом в вебе и файлах. Однобайтовые кодировки оставляют для чтения старых данных.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)