Python: кодировка строки и преобразование в байты
Кодировка строки в Python определяет, какими байтами будет записан текст при сохранении в файл, отправке по сети или передаче в другую программу. В статье разобраны методы encode() и decode(), параметр errors, популярные кодировки и причины ошибок UnicodeEncodeError и UnicodeDecodeError, а также способы исправить нечитаемый текст.

Содержание статьи
Строки и байты в Python 3
В Python 3 есть два разных типа для данных. Тип str хранит текст как последовательность символов Unicode, у каждого символа есть номер (кодовая точка). Тип bytes хранит последовательность чисел от 0 до 255, то есть то, что реально записывается на диск.
Кодировка - это правило перевода символов в байты и обратно. Пока текст находится внутри программы, кодировка не важна: строка 'привет' всегда состоит из шести символов. Кодировка появляется на границе, когда текст нужно записать или прочитать.
text = 'привет'
print(len(text)) # 6 символов
print(ord('п')) # 1087 - кодовая точка
data = text.encode('utf-8')
print(data) # b'\xd0\xbf\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82'
print(len(data)) # 12 байт
print(type(data)) # <class 'bytes'>В UTF-8 каждая русская буква занимает 2 байта, поэтому длина в байтах вдвое больше длины в символах. Подробнее о самом типе байтов рассказано в статье про функцию bytes().
Метод encode(): строка в байты
Метод строки encode() возвращает объект bytes в указанной кодировке. Синтаксис:
str.encode(encoding='utf-8', errors='strict')Оба параметра необязательны. Если их не указать, используется UTF-8 и строгий режим обработки ошибок. Названия кодировок нечувствительны к регистру и допускают варианты: 'utf-8', 'UTF8', 'utf_8' означают одно и то же.
word = 'pythön!'
print(word.encode()) # b'pyth\xc3\xb6n!'
print(word.encode('latin-1')) # b'pyth\xf6n!'
print('Привет'.encode('cp1251')) # b'\xcf\xf0\xe8\xe2\xe5\xf2'Одна и та же буква ö в UTF-8 занимает два байта, а в Latin-1 один. Именно поэтому для чтения байтов нужно знать, в какой кодировке их записали.
Параметр errors и обработка ошибок
Если символ нельзя представить в выбранной кодировке, по умолчанию возникает UnicodeEncodeError. Например, в ASCII нет буквы ö и кириллицы. Параметр errors задает другое поведение:
| Значение errors | Что происходит | Результат для 'pythön!' в ASCII |
|---|---|---|
strict | Исключение (по умолчанию) | UnicodeEncodeError |
ignore | Символ пропускается | b'pythn!' |
replace | Замена на ? | b'pyth?n!' |
xmlcharrefreplace | Ссылка на символ в XML и HTML | b'pythön!' |
backslashreplace | Escape-последовательность | b'pyth\\xf6n!' |
namereplace | Имя символа в формате \N{...} | b'pyth\\N{LATIN SMALL LETTER O WITH DIAERESIS}n!' |
word = 'pythön!'
print(word.encode('ascii', 'ignore')) # b'pythn!'
print(word.encode('ascii', 'replace')) # b'pyth?n!'
try:
word.encode('ascii')
except UnicodeEncodeError as e:
print(e.reason, e.start) # ordinal not in range(128) 4Режим ignore удобен, но молча теряет данные. Используйте его, только если потеря символов допустима, например при генерации технических имен файлов.
Метод decode(): байты в строку
Обратное преобразование выполняет метод decode() у объекта bytes. У строки такого метода нет, поэтому запись 'текст'.decode() вызовет AttributeError. Параметры те же: кодировка и режим обработки ошибок.
raw = b'\xd0\xbf\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82'
print(raw.decode('utf-8')) # привет
print(str(raw, 'utf-8')) # привет, то же самое
broken = b'\xd0\xbf\xd1'
print(broken.decode('utf-8', errors='replace')) # п и символ заменыЕсли байты не соответствуют правилам кодировки, возникает UnicodeDecodeError. Обычно это значит, что файл записан в другой кодировке, например в cp1251, а читается как UTF-8.
Как исправить кракозябры
Нечитаемый текст вроде привет появляется, когда байты UTF-8 прочитали как cp1251. Ошибку можно воспроизвести и обратить:
good = 'привет'
bad = good.encode('utf-8').decode('cp1251')
print(bad) # привет
fixed = bad.encode('cp1251').decode('utf-8')
print(fixed) # приветПринцип простой: закодировать строку обратно в ту кодировку, которой ее ошибочно прочитали, и декодировать в правильной. Если в тексте есть символы, потерянные при неудачном чтении, полностью восстановить его не получится.
Самые частые кодировки в русскоязычных проектах:
- UTF-8 - стандарт для веба, Linux, macOS и исходных файлов Python;
- cp1251 (windows-1251) - старые файлы и программы Windows;
- cp866 - вывод старых консольных утилит Windows;
- utf-16 - некоторые файлы, сохраненные в Windows, начинаются с маркера BOM;
- utf-8-sig - UTF-8 с маркером BOM, так сохраняет CSV Excel.
Полный список поддерживаемых кодеков приведен в разделе Standard Encodings официальной документации.
Кодировка при работе с файлами
Функция open() в текстовом режиме сама вызывает encode() при записи и decode() при чтении. Если параметр encoding не указан, используется кодировка системы. В Linux это обычно UTF-8, а в русской Windows может оказаться cp1251, поэтому один и тот же скрипт ведет себя по-разному. Указывайте кодировку явно:
with open('notes.txt', 'w', encoding='utf-8') as f:
f.write('Заметка')
with open('old.txt', encoding='cp1251') as f:
text = f.read()Больше примеров чтения и записи собрано в статье про текстовые файлы, а работа с файлами в режиме 'rb' разобрана в материале про бинарные файлы. Узнать кодировку по умолчанию для файлов можно вызовом locale.getpreferredencoding().
Частые вопросы
Коротко о том, что чаще всего спрашивают про кодировки строк в Python.
Как узнать кодировку строки?
У объекта str кодировки нет: это уже декодированный текст в Unicode. Кодировка есть только у байтов. Определить ее по байтам можно лишь с некоторой вероятностью, для этого используют сторонние библиотеки, например charset-normalizer.
Чем отличаются UnicodeEncodeError и UnicodeDecodeError?
Первая ошибка возникает при переводе строки в байты, когда символа нет в целевой кодировке. Вторая появляется при переводе байтов в строку, когда байты не соответствуют выбранной кодировке.
Нужно ли писать # -*- coding: utf-8 -*- в начале файла?
В Python 3 исходные файлы по умолчанию читаются как UTF-8, поэтому строка нужна, только если файл сохранен в другой кодировке.
Как убрать из строки все символы, кроме ASCII?
Используйте text.encode('ascii', 'ignore').decode('ascii'). Результат снова будет строкой, но без символов вне ASCII.