Кодировка символов — это правило, которое связывает символ (букву, цифру, знак) с числом, а число с байтами в памяти или файле. ASCII задает 128 символов латиницы и служебных кодов в 7 битах, KOI-8 добавляет к ним кириллицу в 8-м бите, а Unicode и UTF-8 охватывают почти все письменности мира. Ниже я развожу эти понятия, показываю таблицы и байты и разбираю работу с кодировками в Python — чтобы вы перестали получать «кракозябры» вместо текста.
Содержание
Набор символов, кодировка и шрифт: что не путать
Три понятия часто смешивают, а это разные слои. Разделю их на одном примере с буквой «Я».
- Набор символов (charset) отвечает на вопрос «какой номер у символа». В Unicode у «Я» номер (кодовая точка) U+042F. Номер — это еще не байты.
- Кодировка (encoding) отвечает на вопрос «как записать этот номер байтами». В UTF-8 «Я» превращается в два байта: D0 AF.
- Шрифт отвечает на вопрос «как нарисовать символ». Один и тот же номер в разных шрифтах выглядит по-разному, но байты не меняются.
Цепочка выглядит так: «Я» -> кодовая точка U+042F -> байты D0 AF -> глиф шрифта. ASCII и KOI-8 — исторические таблицы, где набор символов и кодировка практически слиты (номер символа равен значению байта). Unicode эти два слоя разделяет: сначала номер, потом отдельное правило записи (UTF-8, UTF-16 и другие).
ASCII: 7 бит и 128 символов
ASCII (American Standard Code for Information Interchange) — базовая кодировка, стандартизованная как ASA X3.4-1963 в 1963 году. Она использует 7 бит на символ, поэтому кодирует ровно 128 позиций с номерами от 0 до 127. Из них 95 — печатные символы (латиница, цифры, пунктуация, пробел), а 33 — управляющие коды (перевод строки, табуляция и подобные), доставшиеся от телетайпов.
| Диапазон (dec) | Что это | Примеры |
|---|---|---|
| 0-31 | управляющие коды | 9 = таб, 10 = перевод строки (LF), 13 = возврат каретки (CR) |
| 32 | пробел | ‘ ‘ |
| 48-57 | цифры | ‘0’ = 48, ‘9’ = 57 |
| 65-90 | латиница A-Z | ‘A’ = 65, ‘Z’ = 90 |
| 97-122 | латиница a-z | ‘a’ = 97, ‘z’ = 122 |
| 127 | DEL | управляющий |
Проверим номера символов в Python. Метод encode("ascii") возвращает объект bytes, а list() показывает числовые значения байтов.
s = "Otus"
data = s.encode("ascii")
print(data) # что за байты получились
print(list(data)) # их числовые значения
b'Otus'
[79, 116, 117, 115]
Латиница и цифры укладываются в ASCII, а вот кириллицы в таблице нет: номера символов «Привет» больше 127, и кодировщик не знает, как их записать 7 битами. Такой код выбросит исключение — показываю его целиком, а не пересказываю.
try:
"Привет".encode("ascii")
except UnicodeEncodeError as e:
print(e)
'ascii' codec can't encode characters in position 0-5: ordinal not in range(128)
Именно из-за этого ограничения появились 8-битные кодировки: восьмой бит дал еще 128 позиций (128-255) под национальные алфавиты. Для кириллицы одной из таких кодировок стала KOI-8.
KOI-8: 8 бит и кириллица
KOI-8 — семейство 8-битных кодировок кириллицы. Первые 128 позиций в нем совпадают с ASCII, а кириллица размещена во второй половине таблицы (номера 128-255). Самый известный вариант — KOI8-R для русского языка: он зарегистрирован в RFC 1489 в 1993 году (автор — Андрей Чернов) и долго был стандартом де-факто для Unix и почты в рунете. KOI8-R опирается на более ранние отраслевые таблицы (в частности, ГОСТ 19768-74).
У KOI-8 необычный порядок букв: кириллица разложена не по алфавиту, а так, что при сбросе восьмого бита получается похожая по звучанию латинская буква (с инверсией регистра). Это наследие 7-битного предшественника KOI-7 и телетайпного кода. Практический смысл был такой: если канал «съедал» восьмой бит, русский текст не превращался в мусор, а оставался читаемой транслитерацией.
Закодируем слово в KOI8-R и посмотрим на байты.
s = "Привет"
koi = s.encode("koi8-r")
print(koi) # байты в KOI8-R
print(len(koi)) # по одному байту на символ
b'\xf0\xd2\xc9\xd7\xc5\xd4'
6
Теперь проверим ту самую особенность порядка байтов: сбросим у каждого байта восьмой бит (операция & 0x7f) и раскодируем остаток как ASCII.
koi = "привет".encode("koi8-r")
stripped = bytes(b & 0x7f for b in koi)
print(stripped.decode("ascii"))
PRIWET
Строчные русские буквы превратились в заглавную латинскую транслитерацию: п->P, р->R, и->I, в->W, е->E, т->T. Регистр инвертировался (это свойство именно KOI-8, а не всех 8-битных кодировок). Важно: KOI-8 кодирует только кириллицу и латиницу и не умеет другие письменности — для многоязычного текста он не подходит.
Unicode и UTF-8: как все связано
Однобайтных кодировок под каждый язык накопилось много, и они несовместимы между собой: один и тот же байт в KOI8-R и в Windows-1251 означает разные буквы. Решением стал Unicode — единый набор символов, где у каждого знака свой уникальный номер (кодовая точка). В версии Unicode 17.0 (сентябрь 2025) закодировано 159 801 символов.
Важно не путать Unicode и UTF-8. Unicode — это набор символов (таблица «символ -> номер»). UTF-8 — это кодировка, то есть правило записи этих номеров байтами. У Unicode несколько кодировок (UTF-8, UTF-16, UTF-32); сегодня в вебе и файлах доминирует UTF-8.
UTF-8 — кодировка переменной длины: символ занимает от 1 до 4 байтов. Латиница ASCII остается однобайтной (UTF-8 обратно совместим с ASCII), базовая кириллица — 2 байта, многие знаки и символы — 3 байта, эмодзи и редкие письменности — 4 байта. Посмотрим на цепочку «символ -> номер -> байты».
ch = "Я"
print(hex(ord(ch))) # номер (кодовая точка) в hex
print(ch.encode("utf-8")) # байты в UTF-8
print(len(ch.encode("utf-8"))) # сколько байтов
0x42f
b'\xd0\xaf'
2
То есть «Я» -> U+042F -> байты D0 AF. Теперь сравним длину в байтах для символов из разных диапазонов.
for ch in ["A", "Я", "€", "🐍"]:
b = ch.encode("utf-8")
print(ch, len(b), b.hex())
A 1 41
Я 2 d0af
€ 3 e282ac
🐍 4 f09f908d
Отсюда практическое следствие: длина строки в символах и ее размер в байтах — разные величины. В Python len("Я") вернет 1 (один символ), а len("Я".encode("utf-8")) вернет 2 (два байта). Это упрощение: видимый символ иногда состоит из нескольких кодовых точек (например, буква с отдельным диакритическим знаком), но для базового текста правило работает.
Практика на Python: encode, decode и обработка ошибок
В Python 3 есть два разных типа: str (текст, последовательность символов) и bytes (последовательность байтов). Переход между ними всегда идет через кодировку:
str.encode(кодировка)превращает текст в байты (текст -> байты);bytes.decode(кодировка)превращает байты обратно в текст (байты -> текст).
Главное правило: декодировать нужно тем же способом, которым кодировали. Если взять байты KOI8-R и раскодировать их как Windows-1251, ошибки не будет — получится тихий неверный результат, «кракозябры».
data = "текст".encode("koi8-r") # закодировали в KOI8-R
print(data.decode("cp1251")) # а читаем как Windows-1251
ФЕЛУФ
Байты остались те же, но другая таблица сопоставила им другие буквы — получился мусор вместо «текст». Такой сбой называют mojibake (искажение из-за несовпадения кодировок). Отдельный случай — когда байты вообще не образуют допустимую последовательность для целевой кодировки: тогда decode бросает UnicodeDecodeError. Параметр errors управляет реакцией на такие байты.
data = "café".encode("utf-8") # байты: b'caf\xc3\xa9'
try:
data.decode("ascii") # строгий режим по умолчанию
except UnicodeDecodeError as e:
print(e)
print(data.decode("ascii", errors="replace")) # заменить на маркер
print(data.decode("ascii", errors="ignore")) # выбросить плохие байты
'ascii' codec can't decode byte 0xc3 in position 3: ordinal not in range(128)
caf��
caf
По умолчанию decode работает в строгом режиме и падает на первом же байте вне диапазона. Режим errors="replace" подставляет символ-заменитель, errors="ignore" молча выбрасывает проблемные байты (данные теряются). Использовать нестрогие режимы стоит осознанно: они прячут проблему, а не решают ее. Правильный путь — знать исходную кодировку и указывать ее явно.
Как избегать mojibake на практике
Несколько приемов, которые снимают большую часть проблем с кодировками:
- Держите весь проект в UTF-8: исходники, базу данных, шаблоны, HTTP-заголовки.
- При открытии файла указывайте кодировку явно:
open("file.txt", encoding="utf-8"). По умолчанию Python берет кодировку системной локали, а она различается на Windows и Linux. - Не «чините» текст повторным перекодированием вслепую: сначала выясните исходную кодировку, потом раскодируйте один раз правильно.
FAQ
Чем KOI-8 отличается от ASCII?
ASCII — 7-битная кодировка на 128 символов: латиница, цифры, пунктуация и управляющие коды, кириллицы в ней нет. KOI-8 — 8-битное расширение: первые 128 позиций совпадают с ASCII, а во второй половине таблицы (128-255) размещена кириллица. Проще говоря, KOI-8 добавляет к ASCII русские буквы, используя восьмой бит.
KOI-8 и Unicode — это одно и то же?
Нет. KOI-8 — однобайтная кодировка только под кириллицу и латиницу, максимум 256 символов. Unicode — это универсальный набор символов почти для всех письменностей мира (в версии 17.0 — 159 801 символов), а записывается он кодировками UTF-8, UTF-16 или UTF-32. KOI-8 сегодня встречается в основном в старых файлах и системах.
Почему в KOI-8 буквы стоят не по алфавиту?
Порядок унаследован от 7-битного кода KOI-7 и телетайпов. Буквы размещены так, что при потере восьмого бита русский текст превращается в читаемую латинскую транслитерацию (с инверсией регистра), например «привет» становится «PRIWET». Это защищало текст в каналах связи, которые не сохраняли восьмой бит.
Почему вместо текста я вижу «кракозябры»?
Это mojibake: байты декодированы не той кодировкой, которой были закодированы. Например, текст сохранен в KOI8-R, а программа читает его как Windows-1251 или UTF-8. Лечится указанием правильной кодировки при чтении. Если байты изначально в UTF-8, читайте их как UTF-8.
Сколько байтов занимает один символ?
Зависит от кодировки. В ASCII и KOI-8 — всегда один байт на символ. В UTF-8 длина переменная: латиница — 1 байт, базовая кириллица — 2 байта, многие символы — 3 байта, эмодзи — 4 байта. Поэтому длина строки в символах и ее размер в байтах — это разные числа.
Какую кодировку выбрать для нового проекта?
UTF-8. Это стандарт де-факто для веба, файлов и баз данных: он совместим с ASCII, покрывает все языки и поддерживается везде. KOI-8 и Windows-1251 нужны, только когда вы работаете со старыми данными, сохраненными в этих кодировках.
Выводы
- Набор символов (какой номер у символа), кодировка (как записать номер байтами) и шрифт (как нарисовать) — три разных слоя, их нельзя смешивать.
- ASCII — 7 бит, 128 символов, только латиница и служебные коды. KOI-8 — 8 бит, добавляет кириллицу во второй половине таблицы.
- Порядок букв в KOI-8 неслучаен: сброс восьмого бита дает латинскую транслитерацию — наследие KOI-7 и телетайпов.
- Unicode — универсальный набор символов, UTF-8 — его кодировка переменной длины (1-4 байта), совместимая с ASCII. Это разные вещи.
- В Python текст и байты связаны через
encode/decode; декодировать нужно той же кодировкой, что и кодировали, иначе выйдет mojibake илиUnicodeDecodeError.
Освойте тему на практике
Хотите уверенно работать со строками, байтами и кодировками на практике и не спотыкаться о «кракозябры» в реальных проектах? Разберите тему на онлайн-курсе Python Basic в Otus — от основ языка до работы с текстом и файлами.
Освойте тему на практике
А чтобы познакомиться с форматом обучения и преподавателями, загляните на бесплатные вебинары и открытые уроки Otus.



