Кодировка кириллицы: почему появляются кракозябры и как их исправить

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

Кодировка кириллицы — это правило, по которому русская буква превращается в число, а число в конкретные байты в файле или памяти. Проблема в том, что таких правил исторически несколько (CP866, KOI8-R, Windows-1251, UTF-8), и одна и та же буква в них записывается разными байтами. Если текст сохранили одной кодировкой, а читают другой, вместо русских слов появляются «кракозябры». Ниже я показываю, чем отличаются эти кодировки, почему у одной буквы получаются разные байты и как распознать и починить испорченный текст в Python.

Набор символов и кодировка: что не путать

Два близких понятия лучше развести сразу, потому что их постоянно смешивают.

  • Набор символов отвечает на вопрос «какой номер у буквы». В 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.
OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)