Кодировка кириллицы — это правило, по которому русская буква превращается в число, а число в конкретные байты в файле или памяти. Проблема в том, что таких правил исторически несколько (CP866, KOI8-R, Windows-1251, UTF-8), и одна и та же буква в них записывается разными байтами. Если текст сохранили одной кодировкой, а читают другой, вместо русских слов появляются «кракозябры». Ниже я показываю, чем отличаются эти кодировки, почему у одной буквы получаются разные байты и как распознать и починить испорченный текст в Python.
Содержание
- Набор символов и кодировка: что не путать
- Почему у кириллицы столько кодировок
- Одна буква — разные байты
- Unicode и UTF-8: почему победили именно они
- Кракозябры: почему возникают и как читать симптом
- Как определить кодировку перебором
- Как починить уже испорченный текст
- Работа с кодировками в файлах
- Где это встречается на практике
- FAQ
- Выводы
Набор символов и кодировка: что не путать
Два близких понятия лучше развести сразу, потому что их постоянно смешивают.
- Набор символов отвечает на вопрос «какой номер у буквы». В Unicode у буквы «Я» номер (кодовая точка) U+042F. Это еще не байты, а просто позиция в общей таблице.
- Кодировка отвечает на вопрос «как записать этот номер байтами». Одну и ту же кодовую точку разные кодировки записывают по-разному.
Старые однобайтные таблицы (CP866, KOI8-R, Windows-1251) устроены проще: в них номер символа сразу равен значению единственного байта, то есть набор символов и кодировка практически слиты. Unicode эти слои разделяет: сначала общий номер, потом отдельное правило записи (UTF-8, UTF-16 и другие). Именно поэтому дальше я аккуратно различаю Unicode как набор символов и UTF-8 как одну из его кодировок.
Почему у кириллицы столько кодировок
В основе всех однобайтных кодировок лежит ASCII — таблица на 128 позиций (номера 0-127), куда входят латиница, цифры, знаки препинания и управляющие коды. На них хватает 7 бит, кириллицы там нет.
Восьмой бит дал еще 128 позиций (номера 128-255), и в эту верхнюю половину разные разработчики поместили кириллицу по-своему. Так появилось несколько несовместимых кодировок:
- CP866 (она же OEM 866, «DOS-кодировка») — использовалась в MS-DOS и русской консоли; в верхней половине рядом с кириллицей лежат символы псевдографики для рамок.
- KOI8-R — основная кодировка для Unix и электронной почты в рунете 1990-х, закреплена в RFC 1489 (1993). Буквы в ней стоят не по алфавиту, а так, что при потере восьмого бита русский текст превращается в читаемую латинскую транслитерацию.
- Windows-1251 (ее же называют CP1251, ANSI-кодировкой) — кодировка кириллицы в Windows; долго была стандартом де-факто для русскоязычного веба и документов.
- ISO 8859-5 — международный стандарт с кириллицей; на практике встречался реже трех предыдущих.
Все они однобайтные: один символ — ровно один байт, максимум 256 позиций. Общая беда однобайтных кодировок — несовместимость: один и тот же байт в CP866, KOI8-R и Windows-1251 означает разные буквы. Отсюда и растут кракозябры.
Одна буква — разные байты
Покажу это на слове «Привет». Закодирую его в четырех кодировках и выведу байты в шестнадцатеричном виде и их количество.
word = "Привет"
for enc in ["cp866", "koi8-r", "cp1251", "utf-8"]:
b = word.encode(enc)
print(f"{enc:8} {b.hex()} байт: {len(b)}")
cp866 8fe0a8a2a5e2 байт: 6
koi8-r f0d2c9d7c5d4 байт: 6
cp1251 cff0e8e2e5f2 байт: 6
utf-8 d09fd180d0b8d0b2d0b5d182 байт: 12
Байты у трех однобайтных кодировок совершенно разные, хотя слово одно. А в UTF-8 их сразу 12: каждая кириллическая буква занимает по 2 байта. Видно и главную причину кракозябр: если записать байты cff0e8e2e5f2 (Windows-1251), а прочитать их по таблице KOI8-R, получится другой набор букв.
Сравню отдельно одну букву «П»: у нее в Unicode номер U+041F (десятичное 1055), но в байтах он выглядит по-разному.
| Кодировка | Байты для «П» (hex) | Длина |
|---|---|---|
| CP866 | 8F | 1 байт |
| KOI8-R | F0 | 1 байт |
| Windows-1251 | CF | 1 байт |
| UTF-8 | D0 9F | 2 байта |
Unicode и UTF-8: почему победили именно они
Держать десятки несовместимых однобайтных таблиц неудобно, поэтому появился Unicode — единый набор символов, где у каждого знака свой уникальный номер (кодовая точка). Unicode охватывает почти все письменности мира — более 150 тысяч символов, кириллица в нем занимает основной блок U+0400-U+04FF и несколько дополнительных.
Сам Unicode задает только номера, а записываются они кодировками: UTF-8, UTF-16, UTF-32. Сегодня в вебе, файлах и базах данных доминирует UTF-8 — кодировка переменной длины: символ занимает от 1 до 4 байтов. Латиница из ASCII остается однобайтной (UTF-8 обратно совместим с ASCII), базовая кириллица занимает 2 байта, многие знаки — 3 байта, эмодзи и редкие письменности — 4 байта.
Посмотрю на цепочку «символ -> номер -> байты» для буквы «Я».
ch = "Я"
print(hex(ord(ch))) # номер (кодовая точка)
print(ch.encode("utf-8")) # байты в UTF-8
print(len(ch.encode("utf-8"))) # сколько байтов
0x42f
b'\xd0\xaf'
2
То есть «Я» -> U+042F -> байты D0 AF. Отсюда практическое следствие: в UTF-8 длина строки в символах и ее размер в байтах — разные величины. Для слова «Привет» это len("Привет") равно 6 (символов), а len("Привет".encode("utf-8")) равно 12 (байтов). Это упрощение: изредка видимый символ состоит из нескольких кодовых точек, но для обычного русского текста правило работает.
Кракозябры: почему возникают и как читать симптом
Кракозябры (по-английски mojibake) — это текст, который прочитали не той кодировкой, которой записали. Байты остаются те же, но другая таблица сопоставляет им другие буквы. Классический случай — когда файл в UTF-8 читают как Windows-1251.
data = "Привет".encode("utf-8") # так текст лежит в файле
print(data.decode("cp1251")) # а программа читает его как Windows-1251
Привет
По виду искажения часто можно понять, что произошло. Собрал типовые симптомы для слова «Привет» в таблицу.
| Что видно на экране | Вероятная причина |
|---|---|
| РџСЂРевет | текст в UTF-8 прочитан как Windows-1251 |
| ╧ЕштхЄ | текст в Windows-1251 прочитан как CP866 (DOS) |
| оПХБЕР | текст в Windows-1251 прочитан как KOI8-R |
| ?????? или пустые прямоугольники | байты потеряны при чтении с заменой символов либо в шрифте нет нужных глифов |
Отдельно стоит случай, когда байты вообще не складываются в допустимую последовательность для целевой кодировки: тогда чтение падает с ошибкой, а не выдает тихий мусор.
data = "Привет".encode("cp1251") # 6 байт Windows-1251
try:
print(data.decode("utf-8")) # пробуем прочитать как UTF-8
except UnicodeDecodeError as e:
print(type(e).__name__, e, sep=": ")
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xcf in position 0: invalid continuation byte
Явная ошибка тут даже полезнее тихих кракозябр: она сразу говорит, что кодировка выбрана неверно.
Как определить кодировку перебором
Если исходная кодировка неизвестна, а текст точно русский, помогает простой перебор: раскодировать байты каждой кодировкой и посмотреть, где получаются осмысленные слова. Читаемый вариант обычно один.
raw = "Привет".encode("cp1251") # байты неизвестного происхождения
for enc in ["utf-8", "cp1251", "cp866", "koi8-r"]:
try:
print(enc, "->", raw.decode(enc))
except UnicodeDecodeError:
print(enc, "-> UnicodeDecodeError")
utf-8 -> UnicodeDecodeError
cp1251 -> Привет
cp866 -> ╧ЁштхЄ
koi8-r -> оПХБЕР
Осмысленно читается только вариант cp1251 — значит, байты были в Windows-1251. Для больших объемов и автоопределения есть готовые библиотеки (например, charset-normalizer), но на коротком фрагменте перебор нагляднее.
Как починить уже испорченный текст
Если текст уже показан кракозябрами, но искажение однотипное, его часто можно развернуть обратно. Логика такая: сначала «обратно» закодировать испорченную строку той кодировкой, которой ее ошибочно прочитали, а полученные байты раскодировать правильной кодировкой.
broken = "Привет" # кракозябры на экране
fixed = broken.encode("cp1251").decode("utf-8") # разворачиваем искажение
print(fixed)
Привет
Здесь текст изначально был в UTF-8, но его прочитали как Windows-1251. Обратная операция encode("cp1251") возвращает исходные байты, а decode("utf-8") читает их верно. Прием работает, только если при первом искажении данные не потерялись (например, чтение шло в строгом режиме, а не с отбрасыванием байтов).
Работа с кодировками в файлах
Больше всего кракозябр возникает при чтении файлов, потому что кодировку по умолчанию Python берет из системной локали, а она разная на Windows и Linux. Надежный путь — указывать кодировку явно. Сымитирую файл в Windows-1251 и прочитаю его.
with open("data.txt", "wb") as f:
f.write("Привет, мир".encode("cp1251")) # файл в Windows-1251
try:
open("data.txt", encoding="utf-8").read() # читаем как UTF-8
except UnicodeDecodeError as e:
print("ошибка:", e)
print(open("data.txt", encoding="cp1251").read()) # читаем верно
ошибка: 'utf-8' codec can't decode byte 0xcf in position 0: invalid continuation byte
Привет, мир
Практические правила простые: держать весь проект в UTF-8 (исходники, базу, шаблоны, HTTP-заголовки), при работе с файлами всегда передавать encoding= явно, а старые данные в CP866, KOI8-R или Windows-1251 приводить к UTF-8 один раз при импорте, а не читать вслепую.
Где это встречается на практике
С кодировками кириллицы чаще всего сталкиваются, когда данные приходят из разных источников: старый CSV в Windows-1251, выгрузка из корпоративной системы в CP866, письмо в KOI8-R, а весь остальной проект — в UTF-8. Аналитик, который читает такие файлы в pandas, и разработчик, который принимает данные по API, решают одну и ту же задачу: определить исходную кодировку и привести все к единому UTF-8, иначе отчеты и выборки заполняются кракозябрами.
Освойте тему на практике
Если хотите уверенно читать и обрабатывать «грязные» данные с разными кодировками, разберите тему на практике на онлайн-курсе Python для аналитики данных в Otus — там работа со строками, файлами и таблицами доведена до реальных задач.
Освойте тему на практике
А чтобы вживую познакомиться с форматом обучения и преподавателями, загляните на бесплатные вебинары и открытые уроки Otus.
FAQ
Чем Windows-1251 отличается от UTF-8?
Windows-1251 — однобайтная кодировка только под кириллицу и латиницу: один символ всегда занимает один байт, всего 256 позиций. UTF-8 — универсальная кодировка Unicode переменной длины (1-4 байта на символ), покрывает почти все письменности мира и совместима с ASCII. Для новых проектов берут UTF-8, а Windows-1251 остается в старых данных.
Как в CP866, KOI8-R и Windows-1251 умещается кириллица, если это один байт?
Первые 128 позиций у всех трех заняты символами ASCII (латиница, цифры, служебные коды). Кириллицу помещают в верхнюю половину таблицы — позиции 128-255, которые открывает восьмой бит. Разработчики расставили русские буквы по этим позициям по-разному, поэтому кодировки и оказались несовместимы.
Можно ли автоматически определить кодировку файла?
Точной гарантии нет: кодировка не записана в самих байтах (у UTF-8 бывает только необязательная метка BOM). На практике помогает перебор кодировок с оценкой осмысленности текста; эту логику реализуют библиотеки вроде charset-normalizer и chardet. На коротком русском фрагменте достаточно перебрать UTF-8, Windows-1251, CP866 и KOI8-R вручную.
Выводы
- Кодировка кириллицы — это правило записи русской буквы байтами; таких правил несколько, и они несовместимы.
- CP866 (DOS), KOI8-R (Unix, почта), Windows-1251 (Windows) — однобайтные: один символ, один байт, максимум 256 позиций, кириллица в верхней половине таблицы.
- UTF-8 — кодировка Unicode переменной длины (базовая кириллица — 2 байта); сегодня это стандарт для веба, файлов и баз данных.
- Кракозябры возникают, когда байты читают не той кодировкой, которой записали; по виду искажения часто понятно, что произошло.
- Лечение: определить исходную кодировку (в том числе перебором), читать файлы с явным
encoding=, весь проект держать в UTF-8.



