Кодировки в Python: почему кракозябры и как их убрать
Открыли CSV — вместо русских слов «Ð¿Ñивеѻ. Разбираем, почему Python отдаёт кракозябры и UnicodeDecodeError, и как чинить конкретные случаи с файлами, запросами и базой.

Вы читаете CSV от бухгалтерии, а вместо «Иванов» видите «Ð˜Ð²Ð°Ð½Ð¾Ð²». Или скрипт падает с UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd0 in position 4. Оба случая — про одно и то же: байты на диске записаны в одной кодировке, а Python читает их в другой. Здесь разберём, как найти реальную кодировку файла, как читать и писать без сюрпризов и что делать с уже испорченным текстом. Актуально для Python 3.9–3.13.
Что происходит на самом деле
В Python 3 строка str — это последовательность символов Unicode, а не байтов. Байты живут в bytes. Между ними два перехода: декодирование (bytes.decode()) и кодирование (str.encode()). Кодировка — это правило, как символ превращается в байты и обратно.
Кракозябры появляются, когда правило на запись и на чтение разное. Файл сохранён в Windows-1251 (одна русская буква — один байт), а вы читаете его как UTF-8 (одна русская буква — два байта). Python берёт байт 0xC8 и пытается сложить из него UTF-8-последовательность — получает либо мусор, либо исключение.
Файл не «в неправильной кодировке». Кодировки нет внутри байтов — она у того, кто их читает. Ваша задача: указать при чтении ту же кодировку, в которой писали.
Два разных симптома
- UnicodeDecodeError — байты не складываются в валидную последовательность выбранной кодировки. Чтение обрывается.
- Кракозябры без ошибки — байты сложились, но в другие символы. Ошибки нет, а текст нечитаемый. Хуже первого случая: скрипт молча пишет мусор дальше.
Шаг первый: узнать реальную кодировку
Гадать не нужно. Байты можно посмотреть напрямую:
with open('data.csv', 'rb') as f: # rb — читаем сырые байты
raw = f.read(200)
print(raw[:60])
# b'\xef\xbb\xbf\xd0\x98...' первые три байта EF BB BF — это BOM UTF-8
# b'\xc8\xe2\xe0\xed\xee\xe2' байты в диапазоне C0-FF по одному на букву — похоже на CP1251
Ориентиры на глаз: русская буква в UTF-8 — это два байта, первый обычно 0xD0 или 0xD1. В Windows-1251 русская буква — один байт из диапазона 0xC0–0xFF. Три байта EF BB BF в начале — метка порядка байтов (BOM1) UTF-8, её оставляет Excel.
Если файлов много и глазами долго — поставьте charset-normalizer (он же тянется как зависимость requests):
# pip install charset-normalizer
from charset_normalizer import from_path
result = from_path('data.csv').best()
print(result.encoding) # например: 'cp1251'
print(str(result)[:100]) # уже декодированный текст для проверки глазами
Определение по содержимому — это догадка по вероятности, а не факт. На коротких файлах ошибается. Всегда сверяйте результат глазами.
Шаг второй: читать с явной кодировкой
Никогда не полагайтесь на кодировку по умолчанию. В open() она зависит от локали ОС: на Linux обычно UTF-8, на Windows до недавнего времени — CP1251. Один и тот же скрипт читает файл по-разному на разных машинах. Указывайте encoding явно всегда:
# Файл в Windows-1251
with open('data.csv', encoding='cp1251') as f:
text = f.read()
# Файл в UTF-8 с BOM от Excel — utf-8-sig срежет BOM автоматически
with open('export.csv', encoding='utf-8-sig') as f:
text = f.read()
Разница между utf-8 и utf-8-sig при чтении: utf-8 оставит BOM внутри текста как невидимый символ \ufeff в начале первой строки. Потом сравнение row[0] == 'Дата' молча не сработает, потому что реально там '\ufeffДата'. При чтении файлов из Excel берите utf-8-sig.
Записывать — тоже с явной кодировкой
# Пишем UTF-8 без BOM — универсальный вариант для дальнейшей обработки
with open('out.csv', 'w', encoding='utf-8') as f:
f.write(text)
# Если файл будут открывать в русском Excel двойным кликом — нужен BOM,
# иначе Excel решит, что это CP1251, и покажет кракозябры
with open('for_excel.csv', 'w', encoding='utf-8-sig') as f:
f.write(text)
Начиная с Python 3.15 — предупреждение об отсутствии encoding
В Python 3.10 добавили механизм, который ловит вызовы open() без явной кодировки. Запустите скрипт с флагом и увидите все места, где кодировка неявная:
python -X warn_default_encoding your_script.py
# EncodingWarning: 'encoding' argument not specified
По планам разработчиков Python поведение по умолчанию для open() в текстовом режиме со временем сдвигается к UTF-8 независимо от локали. Точный статус на вашу версию сверяйте в документации модуля io и PEP 597 — не полагайтесь на то, что «на моей машине и так UTF-8».
Сравнение частых кодировок для русского текста
| Кодировка | Байт на русскую букву | Откуда обычно приходит | Имя в Python |
|---|---|---|---|
| UTF-8 | 2 | Linux, macOS, современные API, JSON | utf-8 |
| UTF-8 с BOM | 2 + метка в начале | Экспорт из Excel | utf-8-sig |
| Windows-1251 | 1 | Старые выгрузки 1С, Windows-программы | cp1251 |
| CP866 | 1 | DOS, вывод старых консольных утилит | cp866 |
| KOI8-R | 1 | Старые Unix-системы, архивная почта | koi8-r |
Если текст уже испорчен
Частый случай: кто-то прочитал CP1251-файл как UTF-8 и сохранил результат. Иногда это обратимо. Классический «моджибейк» — когда UTF-8-байты были ошибочно прочитаны как Windows-1251 и записаны обратно как UTF-8. Тогда помогает обратный проход:
# text содержит 'пÑивеÑ' вместо 'привет'
# Гипотеза: исходные UTF-8 байты прочитали как cp1251
recovered = text.encode('cp1251').decode('utf-8')
print(recovered) # 'привет'
Работает не всегда: если при первой перекодировке символы уже потерялись (заменились на ? или \ufffd), восстановить их нечем — информация утрачена. Проверяйте результат, а не верьте, что «команда сработала». И держите оригинал: чинить лучше из исходных байтов, а не из уже испорченной копии.
Что делать с битыми байтами при чтении
Когда файл частично повреждён, а прочитать надо всё равно, у decode и open есть параметр errors:
errors='strict'— по умолчанию, бросает исключение на первом плохом байте;errors='replace'— подставляет символ\ufffd(ромб с вопросом) вместо битого байта, чтение не падает;errors='ignore'— молча выбрасывает битые байты. Опасно: длина текста меняется, потерю не видно;errors='backslashreplace'— заменяет на экранированную запись байта, удобно для отладки.
with open('broken.log', encoding='utf-8', errors='replace') as f:
for line in f:
process(line) # чтение не оборвётся на битой строке
Это костыль для чтения логов, а не способ «починить кодировку». Для данных, которые вы потом сохраняете, ignore и replace не годятся — вы записываете потерю в файл.
Если не получилось
- UnicodeDecodeError: ...0xd0 in position N — вы читаете UTF-8-файл как CP1251 или наоборот. Посмотрите первые байты через
'rb'и подберите кодировку из таблицы. - Текст читается, но первое поле не сравнивается — в начале сидит BOM
\ufeff. Читайте черезutf-8-sigили срежьтеtext.lstrip('\ufeff'). - UnicodeEncodeError при print в консоли Windows — консоль в CP866, а вы печатаете символ, которого там нет. Установите переменную окружения
PYTHONUTF8=1или запустите с флагом-X utf8. - Кракозябры из базы данных — кодировка не файла, а соединения. Проверьте параметр
charsetв строке подключения и кодировку самой таблицы; для MySQL этоcharset='utf8mb4', документацию драйвера смотрите отдельно. - Данные из requests в кракозябрах —
response.textиспользует кодировку, угаданную по заголовкам. Возьмитеresponse.content.decode('нужная-кодировка')вручную.
Что настроить дальше
Один раз пройдитесь по проекту с python -X warn_default_encoding и добавьте encoding= во все open(). На вход конвейера ставьте нормализацию: любой входящий файл сразу декодируйте в str и внутри системы работайте только с Unicode, а кодировку выбирайте один раз — на границе, где данные уходят наружу. Для проверки перед деплоем полезно прогнать реальные боевые файлы, а не тестовые латиницей — кракозябры вылезают именно на русских именах и адресах.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: Python docs — codecs: Standard Encodings, PEP 597 — Add optional EncodingWarning
Частые вопросы
У меня Python 3.9, а не 3.13 — приёмы сработают?
Как понять, какая кодировка у файла, если charset-normalizer выдаёт разное?
Испорченный текст восстановился не полностью — что делать?
В чём разница между utf-8 и utf-8-sig?
Почему print падает с UnicodeEncodeError, хотя файл читается нормально?
Стоит ли ловить кодировку через errors='ignore', чтобы скрипт не падал?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.