Модуль 5 · Строки

Справочник по Python · модуль 5 · строки

Глава 5.4

5.4

Кодирование строк:
encode() и decode()

Текст в программе имеет тип str, а файл, сеть и база данных передают байты. encode() кодирует символы в bytes, decode() восстанавливает текст по той же кодировке. Несовпадение кодировок даёт либо исключение, либо испорченный текст.

Категория
Строки и байты
Уровень
Средний
Связанные темы
Unicode, файлы, сеть, исключения
Зачем это знать
Корректно переводить текст в передаваемые байты и обратно

§1Зачем нужны байты

Внутри программы строка — это последовательность символов, и глава 5.1 показала, что за каждым символом стоит номер в таблице Unicode. Но ни файл, ни сетевое соединение символов не знают: они умеют передавать только байты — числа от 0 до 255.

Схемаграница между программой и внешним миром
   в программе          на диске и в сети
       str        ←→          bytes
   "Привет"       ←→   b'\xd0\x9f\xd1\x80...'
              encode
              decode

Кодировка — это правило перевода между двумя мирами: какими байтами записывается каждый символ. Пока текст живёт в переменной, кодировка не важна. Она становится важной ровно в двух точках: когда текст покидает программу и когда возвращается в неё.

§2str и bytes

Это два разных типа с разным содержимым и разными методами.

ТипСодержимоеЛитералМетод перевода
strсимволы Unicode"Привет"encode()
bytesчисла от 0 до 255b"hello"decode()

Направление методов запомнить легко: encode() есть только у строки и уводит из текста в байты, decode() есть только у байтов и возвращает обратно. Обратного не бывает — у str нет метода decode(), у bytes нет encode().

§3Литерал b и его содержимое

Байты записываются как строка с буквой b перед кавычкой. Внутри — не текст, а числа: печатаемые ASCII-символы Python показывает как есть, остальные — в шестнадцатеричном виде.

main.pyбайты — это последовательность чисел
data = b"AB"

print(data)
print(data[0])
print(list(data))
Выводобращение по индексу даёт число
b'AB'
65
[65, 66]

Число 65 знакомо по главе 5.1 — это кодовая точка буквы A. Для символов ASCII байт и кодовая точка совпадают, и именно поэтому байты выглядят «как текст». Для кириллицы совпадения уже нет.

§4encode(): str в bytes

encode(encoding="utf-8") переводит строку в байты по указанной кодировке.

main.pyшесть букв кириллицы
text = "Привет"
data = text.encode("utf-8")

print(data)
print(type(data))
Выводдвенадцать байтов
b'\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82'
<class 'bytes'>

Каждая пара вида \xd0\x9f — это два байта, записанных шестнадцатерично. Аргумент можно опустить: text.encode() означает то же самое, что text.encode("utf-8"). Явное указание кодировки надёжнее — оно документирует намерение.

§5decode(): bytes в str

Обратный метод принадлежит объекту bytes и требует указать, по какой кодировке читать байты.

main.pyдекодирование UTF-8
data = "Привет".encode("utf-8")
text = data.decode("utf-8")

print(text)
print(type(text))
Выводтекст восстановлен
Привет
<class 'str'>
Байты не содержат имени кодировки

В b'\xd0\x9f' нет пометки «это UTF-8». Кодировку сообщает формат обмена — заголовок HTTP, спецификация файла, договорённость между системами. Если её никто не сообщил, остаётся угадывать, и это главный источник испорченного текста.

§6Полный маршрут

Пара методов обратима, пока кодировка с обеих сторон одна и та же.

strencode →bytes
bytesdecode →str
main.pyпроверка полного маршрута
original = "Цена: 500 ₽"

payload = original.encode("utf-8")
restored = payload.decode("utf-8")

print(restored == original)
Выводданные совпали
True

§7Длина в символах и в байтах

Это то место, где расхождение двух миров впервые становится заметным.

main.pyодна буква
text = "Я"
data = text.encode("utf-8")

print(len(text))
print(len(data))
print(data)
Вывододин символ, два байта
1
2
b'\xd0\xaf'

Для целой фразы разница накапливается.

main.pyсмешанный текст
text = "Цена: 500 ₽"

print(len(text))
print(len(text.encode("utf-8")))
Вывододиннадцать символов, семнадцать байтов
11
17

Практический вывод: если внешнее ограничение задано в байтах — размер поля в базе, лимит SMS, максимальная длина сообщения в протоколе, — проверять его через len(text) нельзя. Считать нужно len(text.encode("utf-8")).

§8Сколько занимает символ

UTF-8 — кодировка переменной длины: разные символы занимают разное число байтов. Это её главное свойство и причина популярности.

СимволБайтов в UTF-8Что за диапазон
A, 7, пробел1латиница и знаки ASCII
Я, ё2кириллица, греческий, иврит
₽, €3знаки валют, иероглифы
эмодзи4дополнительные плоскости Unicode

Отсюда важное следствие: обратиться к «третьему символу» через индекс байтов нельзя. Индекс в bytes — это позиция байта, и она не совпадает с позицией символа. Резать текст нужно до кодирования, а не после.

§9Выбор кодировки

КодировкаЧто поддерживаетЗамечание
utf-8весь Unicodeстандартный выбор для новых форматов
ascii128 базовых символовкириллица не входит вовсе
latin-1256 позиций, по байту на символдекодирует любые байты, но чаще всего неверно
cp1251кириллица, по байту на символвстречается в старых файлах и базах

Практическое правило: для нового кода — всегда utf-8. Остальные кодировки нужны, когда вы читаете чужие данные и их формат задан заранее.

main.pyта же строка в другой кодировке
text = "привет"

print(text.encode("utf-8"))
print(text.encode("cp1251"))
Выводдвенадцать байтов против шести
b'\xd0\xbf\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82'
b'\xef\xf0\xe8\xe2\xe5\xf2'

Одна и та же строка превращается в совершенно разные наборы байтов. Поэтому байты без указанной кодировки — это данные без смысла.

§10Ошибка кодирования

UnicodeEncodeError возникает, когда в кодировке просто нет нужного символа.

main.pyкириллица в ASCII не помещается
print("Привет".encode("ascii"))
Ошибкакодировать нечем
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-5: ordinal not in range(128)

Сообщение точное: указаны позиции проблемных символов и причина — их кодовые точки выходят за range(128). Это ошибка на выходе из программы.

§11Ошибка декодирования

UnicodeDecodeError — зеркальный случай: байты пришли, но по выбранной кодировке они не складываются в символ.

main.pyбайты UTF-8 читаются как ASCII
data = "Я".encode("utf-8")
print(data.decode("ascii"))
Ошибкабайт вне допустимого диапазона
UnicodeDecodeError: 'ascii' codec can't decode byte 0xd0 in position 0: ordinal not in range(128)

Здесь ошибка сработала как защита: неверная кодировка обнаружена сразу. К сожалению, так везёт не всегда — об этом следующий параграф.

§12Тихая порча текста

Кодировка latin-1 отображает все 256 значений байта, поэтому декодировать ею можно любые данные. Исключения не будет — будет бессмыслица.

main.pyUTF-8 прочитан как latin-1
data = "Привет".encode("utf-8")
print(data.decode("latin-1"))
Выводошибки нет, текста тоже нет
ÐÑивеÑ

Такой результат называют «кракозябрами». Каждый байт двухбайтового символа прочитан как отдельная буква, поэтому шесть букв превратились в двенадцать знаков. Программа продолжает работать, данные молча испорчены — и обнаружится это обычно у пользователя.

Отсутствие ошибки ничего не доказывает

Успешный decode() не означает, что кодировка выбрана верно. Проверять нужно результат: если в тексте появились Ð, Ñ, Â или вопросительные знаки в ромбах, кодировка не та.

Хорошая новость: такая порча обратима, пока текст не сохранён дальше. Достаточно проделать обратный путь.

main.pyлечение кракозябр
broken = "Привет".encode("utf-8").decode("latin-1")
fixed = broken.encode("latin-1").decode("utf-8")

print(fixed)
Выводтекст восстановлен
Привет

§13Параметр errors

Второй параметр обоих методов задаёт, что делать с символами, которые не переводятся. По умолчанию — strict, то есть исключение.

main.pyтри стратегии вместо ошибки
text = "Цена €"

print(text.encode("ascii", errors="ignore"))
print(text.encode("ascii", errors="replace"))
print(text.encode("ascii", errors="backslashreplace"))
Выводпропуск, знак вопроса и escape-запись
b' '
b'???? ?'
b'\\u0426\\u0435\\u043d\\u0430 \\u20ac'
ЗначениеЧто делаетКогда уместно
strictвызывает исключениепо умолчанию; данные важны
ignoreмолча выбрасывает символпочти никогда
replaceподставляет ?логи, отладочный вывод
backslashreplaceзаписывает код символаотладка: видно, что потерялось

Первая строка вывода особенно показательна: от "Цена €" остался один пробел. Режим ignore удалил всё, что не поместилось, и не сообщил об этом. Ставить его «чтобы не падало» — верный способ потерять данные незаметно.

§14Где это встречается

В обычном коде encode() и decode() вызывают редко: файловые и сетевые библиотеки делают это сами. Но кодировку они берут из параметра, и вот там о ней придётся вспомнить.

СитуацияГде задаётся кодировка
чтение и запись файлаopen(path, encoding="utf-8")
ответ веб-серверазаголовок Content-Type
подключение к базе данныхпараметры соединения
лимит на размер сообщенияlen(text.encode("utf-8"))

Работа с файлами разбирается в модуле 8. Здесь достаточно запомнить: если функция принимает параметр encoding, его лучше указать явно, а не полагаться на настройки системы, которые на другой машине окажутся иными.

§15Частые ошибки

Что написаноЧто произойдётКак правильно
text.decode()AttributeError: у str нет такого методадекодировать объект bytes
data.encode()AttributeError: у bytes нет такого методакодировать str
кодировать в UTF-8, декодировать как ASCIIUnicodeDecodeErrorиспользовать согласованную кодировку
декодировать чужие байты как latin-1ошибки не будет, текст превратится в кракозябрыузнать настоящую кодировку источника
считать один символ одним байтомлимит размера вычислится неверноlen(text.encode("utf-8"))
резать bytes по индексу как текстсимвол разорвётся посерединерезать строку до кодирования
errors="ignore", чтобы «не падало»непереводимые символы исчезнут молчаstrict и обработка исключения
полагаться на кодировку по умолчанию в open()на другой машине текст прочитается иначеуказывать encoding="utf-8" явно

§16Проверьте себя

Какой тип возвращает encode()?

bytes — последовательность чисел от 0 до 255.

Почему кодировку нужно знать при декодировании?

Байты не содержат сведений о себе: одни и те же значения в разных кодировках означают разные символы.

Почему len("Я") равно 1, а len("Я".encode("utf-8")) — 2?

Первое считает символы, второе — байты. В UTF-8 кириллическая буква занимает два байта.

Что опаснее: UnicodeDecodeError или успешное декодирование не той кодировкой?

Второе. Исключение сразу указывает на проблему, а тихая порча обнаружится только по испорченному тексту у пользователя.

Чем плох errors="ignore"?

Он молча удаляет данные: от "Цена €" в ASCII останется один пробел, и никакого сообщения об этом не будет.

Можно ли взять третий символ строки из её байтов по индексу 2?

Нет. В UTF-8 символы занимают разное число байтов, поэтому позиция байта не совпадает с позицией символа.

§17Шпаргалка

Текст в байтыdata = text.encode("utf-8")
Байты в текстtext = data.decode("utf-8")
Размер сообщенияlen(text.encode("utf-8"))
Файлopen(path, encoding="utf-8")
Отладка потерьerrors="backslashreplace"
Лечение кракозябрbad.encode("latin-1").decode("utf-8")

Итог главы

encode() создаёт байты из строки, decode() создаёт строку из байтов, и обратимость требует одной и той же кодировки с обеих сторон. Сами байты своей кодировки не хранят — её задаёт формат обмена. Несовпадение проявляется двумя способами: исключением, что удобно, или тихой порчей текста, что гораздо хуже. Для нового кода выбор один — utf-8, указанный явно.

Глава 5.4 · encode() и decode() · Справочник по Python Автор: Макаров Максим Николаевич