Кодировка символов — это правило, по которому текстовый символ представляется набором байтов для хранения и передачи. Важно сразу развести два понятия: набор символов (character set) отвечает на вопрос «какой номер у символа», а кодировка (encoding) — «какими байтами этот номер записать». Unicode — это набор символов, а UTF-8 — одна из его кодировок.
Содержание
- Сквозная цепочка: символ, кодовая точка, байты
- Набор символов и кодировка: не путать
- Однобайтовые кодировки: ASCII, Windows-1251 и родня
- Unicode и его формы: UTF-8, UTF-16, UTF-32
- UTF-8 подробно: почему до 4 байтов
- Кодовая точка и видимый символ — разные вещи
- Кракозябры: чтение не в той кодировке
- Выводы
- Где применяется и что учить дальше
- FAQ
Ниже разберем сквозную цепочку «символ -> кодовая точка -> байты» на живых примерах, сравним однобайтовые кодировки с Unicode, детально посмотрим UTF-8 и типичную ошибку с «кракозябрами». Весь код в статье запущен, вывод в блоках — реальный.
Сквозная цепочка: символ, кодовая точка, байты
Любое кодирование текста проходит три уровня. Возьмем букву Я:
- символ — то, что видит человек:
Я; - кодовая точка (code point) — номер символа в Unicode:
U+042F(это 1071 в десятичной); - байты — как этот номер записан в конкретной кодировке: в UTF-8 это два байта
D0 AF.
Проследим цепочку сразу для нескольких символов в трех кодировках Unicode:
for ch in ['A', 'Я', '中', '😀']:
cp = ord(ch)
b8 = ch.encode('utf-8')
b16 = ch.encode('utf-16-be')
b32 = ch.encode('utf-32-be')
print(f"{ch} U+{cp:04X} utf-8={b8.hex(' ')} ({len(b8)}б) "
f"utf-16={b16.hex(' ')} ({len(b16)}б) utf-32={b32.hex(' ')} ({len(b32)}б)")
Вывод программы:
A U+0041 utf-8=41 (1б) utf-16=00 41 (2б) utf-32=00 00 00 41 (4б)
Я U+042F utf-8=d0 af (2б) utf-16=04 2f (2б) utf-32=00 00 04 2f (4б)
中 U+4E2D utf-8=e4 b8 ad (3б) utf-16=4e 2d (2б) utf-32=00 00 4e 2d (4б)
😀 U+1F600 utf-8=f0 9f 98 80 (4б) utf-16=d8 3d de 00 (4б) utf-32=00 01 f6 00 (4б)
Видно главное: кодовая точка у символа одна и та же, а вот число байтов зависит от кодировки. В UTF-8 латинская A заняла 1 байт, кириллическая Я — 2, иероглиф — 3, эмодзи — 4. В UTF-32 любой символ — ровно 4 байта. Здесь взят вариант с явным порядком байтов (utf-16-be/utf-32-be), чтобы показать чистые байты без служебной метки.
Набор символов и кодировка: не путать
Из цепочки следует важное разграничение. Unicode — это огромная таблица, которая назначает каждому символу номер (кодовую точку) в диапазоне от U+0000 до U+10FFFF. Сам по себе Unicode не говорит, сколькими байтами записать номер.
Как именно превратить номер в байты — решает кодировка. UTF-8, UTF-16 и UTF-32 — это три разные кодировки одного и того же набора Unicode. Поэтому фраза «сохранить в Unicode» неточна: файл сохраняют в конкретной кодировке (чаще всего в UTF-8).
Отдельно стоят старые однобайтовые кодировки. В них набор символов и способ записи слиты: один байт (0-255) сразу задает и номер, и представление символа.
Однобайтовые кодировки: ASCII, Windows-1251 и родня
Исторически первой была ASCII — 7-битная кодировка на 128 символов (номера 0-127): латиница, цифры, знаки препинания и управляющие коды. Кириллицы в ней нет.
Один байт вмещает 256 значений, поэтому на базе ASCII выросли однобайтовые кодировки, где вторая половина (128-255) отдана под национальные алфавиты. Для русского языка их несколько:
| Кодировка | Байт на символ | Где встречается |
|---|---|---|
| ASCII | 1 (7 бит) | латиница, основа для остальных |
| Windows-1251 | 1 | русский текст в старых системах Windows |
| KOI8-R | 1 | Unix-среды, письма 1990-2000-х |
| CP866 | 1 | DOS и консоль Windows |
Проблема однобайтовых кодировок одна: 256 значений мало. Одновременно поддержать русский, греческий и китайский в одном файле нельзя, а один и тот же байт в Windows-1251 и KOI8-R означает разные буквы. Именно из-за этого текст, прочитанный не в той кодировке, превращается в «кракозябры». Эту задачу и решил Unicode.
Unicode и его формы: UTF-8, UTF-16, UTF-32
Unicode задает единый номер каждому символу всех письменностей, эмодзи и множества служебных знаков. Всего под символы отведен диапазон U+0000—U+10FFFF, это чуть больше миллиона возможных позиций; реально назначено больше 150 тысяч символов. Кодировки семейства UTF отличаются тем, как они пакуют эти номера в байты.
| Кодировка | Байт на символ | Особенность |
|---|---|---|
| UTF-8 | 1-4 (переменная) | латиница = 1 байт, совместима с ASCII |
| UTF-16 | 2 или 4 (переменная) | базовые символы — 2 байта, редкие — суррогатная пара из 4 |
| UTF-32 | 4 (фиксированная) | любой символ ровно 4 байта, прямая индексация |
UTF-32 прост и позволяет обращаться к символу по индексу, но расходует память: даже латинская буква занимает 4 байта. UTF-16 экономнее на основном диапазоне, но тоже переменной длины — символы вне базовой плоскости (например, эмодзи) кодируются парой из четырех байтов. Самой распространенной в вебе стала UTF-8.
UTF-8 подробно: почему до 4 байтов
UTF-8 — кодировка переменной длины: символ занимает от 1 до 4 байтов. Название с цифрой 8 означает, что минимальная единица — 8 бит (один байт), а не то, что символ всегда однобайтовый.
Число байтов определяется диапазоном кодовой точки, а старшие биты первого байта сообщают декодеру, сколько байтов читать:
| Диапазон кодовых точек | Байтов | Шаблон битов |
|---|---|---|
| U+0000 — U+007F | 1 | 0xxxxxxx |
| U+0080 — U+07FF | 2 | 110xxxxx 10xxxxxx |
| U+0800 — U+FFFF | 3 | 1110xxxx 10xxxxxx 10xxxxxx |
| U+10000 — U+10FFFF | 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
Потолок в 4 байта не случаен. Первая схема UTF-8 (RFC 2279) допускала последовательности до 6 байтов, но стандарт RFC 3629 в 2003 году ограничил ее 4 байтами — ровно настолько, чтобы покрыть весь диапазон Unicode до U+10FFFF. Поэтому утверждение «UTF-8 кодирует символ вплоть до 6 байтов» устарело: сегодня максимум — 4 байта.
Ключевое достоинство UTF-8 — обратная совместимость с ASCII. Байты 0-127 совпадают с ASCII один в один, поэтому старый ASCII-текст является корректным UTF-8 без изменений. У латиницы это дает и экономию: английский текст в UTF-8 весит столько же, сколько в ASCII, а не вдвое больше, как в UTF-16.
Кодовая точка и видимый символ — разные вещи
Легко решить, что один видимый знак — это всегда одна кодовая точка. Это упрощение, и оно неверно: видимый символ (его называют графемой) может складываться из нескольких кодовых точек.
accent = 'е́'
print('строка выглядит как одна буква:', accent)
print('а кодовых точек в ней:', len(accent))
print('точки:', [f'U+{ord(c):04X}' for c in accent])
family = '👨👩👧'
print('emoji-семья, кодовых точек:', len(family))
print('в utf-8 байт:', len(family.encode('utf-8')))
Вывод программы:
строка выглядит как одна буква: е́
а кодовых точек в ней: 2
точки: ['U+0435', 'U+0301']
emoji-семья, кодовых точек: 5
в utf-8 байт: 18
Буква с ударением е́ — это база е (U+0435) плюс комбинируемый акут (U+0301), то есть две кодовые точки на один видимый знак. Эмодзи-семья 👨👩👧 собрана из трех фигурок, склеенных невидимым соединителем ZWJ (U+200D), — пять кодовых точек и 18 байтов в UTF-8. Поэтому подсчет «длины» текста зависит от того, что считать: байты, кодовые точки или графемы.
Кракозябры: чтение не в той кодировке
Самый частый практический сбой — прочитать байты не в той кодировке, в которой их записали. Байты Я в UTF-8 (D0 AF) не являются корректным ASCII, и попытка разобрать их как ASCII падает с ошибкой:
data = 'Я'.encode('utf-8')
print('байты Я в utf-8:', data)
try:
data.decode('ascii')
except UnicodeDecodeError as e:
print('ошибка:', e)
print('правильно:', data.decode('utf-8'))
Вывод программы:
байты Я в utf-8: b'\xd0\xaf'
ошибка: 'ascii' codec can't decode byte 0xd0 in position 0: ordinal not in range(128)
правильно: Я
Здесь виден правильный рабочий процесс: те же байты, что записаны в UTF-8, надо и читать как UTF-8. А теперь посмотрим, сколько места занимает одна строка в разных кодировках и что бывает, когда символ не помещается:
s = 'Привет, мир! 😀'
for enc in ['utf-8', 'utf-16', 'utf-32', 'cp1251']:
try:
n = len(s.encode(enc))
print(f'{enc:8}: {n} байт')
except UnicodeEncodeError as e:
print(f'{enc:8}: не кодируется ({e.reason})')
Вывод программы:
utf-8 : 26 байт
utf-16 : 32 байт
utf-32 : 60 байт
cp1251 : не кодируется (character maps to <undefined>)
Числа для utf-16 и utf-32 включают служебную метку порядка байтов (BOM) в начале — 2 и 4 байта соответственно, поэтому они чуть больше «чистого» размера. Windows-1251 споткнулась на эмодзи: в ее таблице такого символа просто нет. Это и есть корень «кракозябр» — однобайтовая кодировка физически не вмещает символ, а при неверном чтении подставляет что попало. Практический вывод: держите весь конвейер (файлы, HTTP-заголовок charset=utf-8, базу данных) в UTF-8.
Выводы
- Кодировка символов — правило превращения символа в байты; отдельно живут набор символов (номер символа) и кодировка (запись номера байтами).
- Сквозная цепочка: символ
Я-> кодовая точкаU+042F-> байтыD0 AFв UTF-8; кодовая точка одна, а число байтов зависит от кодировки. - Однобайтовые кодировки (ASCII, Windows-1251, KOI8-R, CP866) вмещают лишь 256 значений и несовместимы между собой — отсюда «кракозябры».
- UTF-8 — переменная длина от 1 до 4 байтов (не 6), совместима с ASCII и стала стандартом де-факто в вебе.
- Видимый символ (графема) может состоять из нескольких кодовых точек, поэтому «длина строки» неоднозначна.
Где применяется и что учить дальше
Понимание кодировок нужно везде, где программа читает или пишет текст: разбор файлов и CSV, работа с HTTP и JSON, обращения к базе данных, парсинг логов. Классические баги новичка — «кракозябры» в консоли, UnicodeDecodeError при открытии файла, обрезанная посередине многобайтовая буква — решаются именно знанием цепочки «символ -> кодовая точка -> байты».
Освойте тему на практике
В Python вся эта механика встроена в тип str и методы encode/decode, и разобраться в ней проще всего на практике. Системно освоить строки, байты и работу с файлами помогает курс Основы программирования на Python. Оценить формат и уровень до старта можно на бесплатных открытых уроках Otus — это живые занятия с преподавателями.
Смежные темы: Формат ISO и стандарты представления данных.
FAQ
Что выбрать для нового проекта — UTF-8 или UTF-16? Для файлов, веба и обмена данными по умолчанию берут UTF-8: она компактна на латинице и совместима с ASCII. UTF-16 остается внутренним форматом строк в некоторых платформах (например, в Java и Windows API), но для хранения и передачи чаще выбирают UTF-8.
Почему при подсчете символов число иногда не совпадает с ожидаемым? Потому что можно считать разное: байты, кодовые точки или видимые графемы. Эмодзи или буква с комбинируемым знаком — один видимый символ, но несколько кодовых точек и несколько байтов.
Что такое BOM и нужен ли он? BOM (byte order mark) — невидимая метка в начале файла, задающая порядок байтов и кодировку. Для UTF-16 и UTF-32 он указывает порядок байтов; для UTF-8 он не обязателен и часто мешает, поэтому файлы обычно сохраняют в UTF-8 без BOM.



