Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 14 из 20 Начальный 20 мин 120 XP

Юникод: ord, chr и кодировки

Каждый символ — это число из таблицы Юникода. Учимся видеть строку глазами компьютера: ord и chr, байты через encode, и главная боль новичков — кракозябры из-за кодировок.

Редакция Питоники

Компьютер не хранит буквы — он хранит числа. Строка «Привет» для Python — последовательность кодов Юникода: 1052, 1088, 1080… Функции ord (код символа) и chr (символ по коду) открывают этот внутренний мир, а понимание кодировок объясняет самую загадочную ошибку новичков — текст из кракозябр.

ord и chr: символ как число

ord(символ) — код одного символа в таблице Юникода, chr(код) — обратное превращение. Латинские буквы и цифры сидят в таблице компактно и по порядку: коды A-Z идут подряд — этим пользуются для простых преобразований.

код символа и символ по коду
print(ord('А'), ord('Я'))
print(chr(1050), chr(1054))

print(chr(ord('a') + 1))
Вывод
1040 1071
К О
b

Русские буквы тоже живут в Юникоде своими кодами: А — 1040. Последняя строка — классический трюк: код буквы «a» плюс один — «b». Арифметика кодов часто экономит целую таблицу замен.

encode и decode: строка и байты

Строка в Python — абстракция из символов. Для записи в файл или сеть её превращают в байты методом encode('utf-8'), а байты обратно в строку — decode('utf-8'). Русская буква в utf-8 занимает два байта, латинская — один; поэтому байты и символы — разные вещи, и путать их нельзя.

строка, байты и обратно
word = 'мир'
encoded = word.encode('utf-8')
print(encoded)
print(len(word), len(encoded))

print(encoded.decode('utf-8'))
Вывод
b'\xd0\xbc\xd0\xb8\xd1\x80'
3 6
мир

Байтовая строка печатается с префиксом b и кодами вида \xd0 — это те же три символа «мир», но в шести байтах utf-8: каждая русская буква заняла два байта. Обратный decode вернул исходный текст без потерь.

Кракозябры: что это и почему

Кракозябры — это байты одной кодировки, прочитанные как другая. Файл сохранили в cp1251 (старая русская кодировка Windows), а открыли как utf-8 — Python честно показал то, что увидел: «Ïðèâåò» вместо «Привет». Данные не испорчены — испорчена интерпретация; правильная кодировка возвращает текст.

одна строка — две кодировки
text = 'Привет'
wrong = text.encode('cp1251').decode('latin-1')
print(wrong)

fixed = wrong.encode('latin-1').decode('cp1251')
print(fixed)
Вывод
Ïðèâåò
Привет
ОперацияЧто делаетПример
ord(символ)Код символа в Юникодеord('А') → 1040
chr(код)Символ по кодуchr(1050) → 'К'
s.encode('utf-8')Строка → байтыb'\xd0\xbc...'
b.decode('utf-8')Байты → строкаb'...' → 'мир'

Главный вывод курса по кодировкам: строки в Python всегда юникодные и всегда правильные; проблемы живут на границах — в файлах и сети. Указывайте кодировку явно при open, и кракозябры обойдут вас стороной.

Сколько байт в символе: utf-8

Юникод — это таблица символов, а не формат хранения. Строка из символов превращается в байты по правилам кодировки, и в utf-8 (стандарт Python 3) размер зависит от символа: ASCII занимает один байт, кириллица и латиница с диакритикой — два, а символы вне базовой плоскости — четыре.

символы и их вес в байтах
for ch in 'aбЯ1':
    print(repr(ch), ord(ch), len(ch.encode('utf-8')))
Вывод
'a' 97 1
'б' 1073 2
'Я' 1071 2
'1' 49 1

Вот главное различие, которое ломает расчёты: длина строки и число байт — разные вещи. len('привет') вернёт 6 символов, а в файле utf-8 этот текст займёт 12 байт. Для кириллицы всегда удваивайте, для латиницы — нет.

символы против байтов
print(len('привет'), len('привет'.encode('utf-8')))
print(len('hello'), len('hello'.encode('utf-8')))
Вывод
6 12
5 5

Это различие объясняет, почему файл с русским текстом «весит больше, чем кажется», и почему лимит загрузки считают в байтах, а не в символах. Оно же — источник классической ошибки: код, который режет файл по байтам, а потом ищет в нём символы, может разрезать кириллицу пополам и получить «сломанный» текст в виде символа-замены.

Диапазон кодовЧто тамПримеры
0–127ASCII: латиница, цифры, знаки'A' → 65, '0' → 48
1040–1103кириллица без ё'А' → 1040, 'я' → 1103
1105ё'ё' → 1105
8200–8239кириллическая пунктуация'—' → 8212
119964+символы вне базовой плоскости'𝔸' → 120120

Кодировки в реальном файле

Теория становится понятной, когда встречаешь файл, который читается не так. Классический случай: выгрузка из 1С или старого учётного сервера в cp1251, которую открывают в Python как utf-8. Текст вроде бы русский, но вместо букв — «кракозябры», и никакого исключения: кодировка «проходит», просто неправильно.

один и тот же файл в двух кодировках
raw = 'Привет'.encode('cp1251')
print(raw)
print(raw.decode('latin-1'))
print(raw.decode('cp1251'))
Вывод
b'\xcf\xf0\xe8\xe2\xe5\xf2'
Ïðèâåò
Привет

Второй вывод — те самые «кракозябры»: байты cp1251, прочитанные как latin-1, где каждый байт — отдельный символ. Третий — то, что должно было получиться: та же строка, прочитанная своей кодировкой. Данные не испорчены, испорчено было только понимание, в какой кодировке они записаны.

ошибка декодирования вместо кракозябр
raw = 'Привет'.encode('cp1251')
try:
    print(raw.decode('utf-8'))
except UnicodeDecodeError as e:
    print('ошибка декодирования:', e.reason)
Вывод
ошибка декодирования: invalid continuation byte

Параметр errors='replace' подставляет «?» вместо символов, которые не удалось декодировать, и программа работает дальше — но часть текста потеряна навсегда. Для чтения файлов replace лучше, чем падение, но молчаливая потеря данных хуже явной ошибки: лучше узнать о проблеме с кодировкой сразу, чем полгода искать пропавшие символы в отчёте.

КодировкаЧто этоКогда встретится
utf-8современный стандарт, по умолчанию в Python 3все новые файлы и весь веб
cp1251старая русская Windows, 1 байт на символфайлы из 1С и старых программ
latin-1первые 256 байт, почти как ASCIIстарые веб-страницы, «кракозябры»
asciiтолько 128 символовконфиги и протоколы
koi8-rсоветская кириллицаредко, но в старых unix-системах

Практический порядок работы с файлом всегда одинаков: открыть с явной кодировкой, прочитать, и только потом решать, что делать с сомнительными символами. Если кодировка неизвестна и файл небольшой — можно перебрать варианты и выбрать тот, где нет «кракозябр»; для больших файлов выручает charset_normalizer.

И отдельно про JSON и HTTP — два места, где кодировка задаётся протоколом, а не вами. JSON по спецификации всегда UTF-8, и json.loads разберётся сам. В HTTP ответ почти всегда приходит в UTF-8, но сервер может объявить другую кодировку в заголовке Content-Type — и тогда response.text возьмёт объявленную, а response.content.decode('utf-8') — принудительно UTF-8. Разница видна на страницах с windows-1251, и это отдельная история из курса про requests.

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

И последнее, что стоит запомнить про весь урок: в Python 3 строки всегда юникодные. Проблемы с кодировками живут не внутри программы, а на её границах — при чтении файлов, при сетевых запросах, при выводе в консоль. Рабочее правило простое: внутри программы работайте со строками, а байты трогайте только там, где они действительно нужны — и всегда указывайте кодировку явно.

Кодировки и escape-последовательности — две стороны одной медали: и то, и другое решает, как символ попадёт в строку и как он оттуда выйдет. Про экранирование подробно в уроке 13, а как строки превращаются в байты при отправке по сети — в уроке про requests.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

word = 'мир'
print(len(word), len(word.encode('utf-8')))
print(chr(1050) + chr(1054) + chr(1052))
print(len('A'.encode('utf-8')), len('Ж'.encode('utf-8')))
Проверь себя
0 / 8

1. Что вернёт ord('A')?

2. Сколько байтов займёт 'мир' в utf-8?

3. Что такое кракозябры в файле?

4. Как прочитать файл старой российской системы с кириллицей?

5. Что вернёт chr(ord('b') - 1)?

6. Что вернёт ord('А')?

7. Зачем нужны ord и chr, если есть готовые методы строк?

8. Что вернёт len('привет'.encode('utf-8'))?

Карточки терминов
Запомнено: 0 / 6
Практика

Для слова 'yes' напечатайте код первой буквы через ord, соберите слово 'no' из кодов 110 и 111 через chr, и напечатайте длину в байтах строки 'привет' в utf-8.

practice.py
Вопросы и ответы по уроку

Что лучше: строки или байты в Python 3?

Всегда работайте со строками (str) внутри программы. Байты — только на границах: чтение файла, сетевой запрос. Байтовые строки (тип bytes) не поддерживают методы строк и всегда печатаются с префиксом b.

Какая кодировка «правильная»?

utf-8 — мировой стандарт и дефолт Python 3: она хранит любые языки. cp1251 — наследие старых русских систем; её поддерживают для чтения старых файлов, но новые данные хранят в utf-8.

Почему len('мир') = 3, а в файле 6 байтов?

len у строки считает символы Юникода. В файле символы закодированы байтами, и русская буква в utf-8 занимает два байта. Обе цифры верны — они про разные вещи: символы и байты.

Что такое Юникод в Python и зачем он нужен?

Юникод — общая таблица символов, где каждому знаку присвоен номер: кириллица, иероглифы, эмодзи. В Python 3 строки всегда юникодные, поэтому в одной строке свободно смешиваются языки. Проблемы с кодировками живут не внутри программы, а на границах: при чтении файлов и в сетевых запросах.

Урок 13: экранирование

Как узнать код символа и как получить символ по коду?

Функция ord возвращает код символа: ord('А') даёт 1040. Функция chr делает обратное: chr(1050) вернёт 'К'. Это полезно для арифметики по кодам (сдвиг на 1 превращает 'a' в 'b') и для работы с бинарными протоколами, где символы кодируются числами.

Почему len('привет') = 6, а в файле 12 байт?

len у строки считает символы Юникода, а в файле символы закодированы байтами: русская буква в utf-8 занимает два байта. Обе цифры верны — они про разные вещи. Для латиницы разницы нет: len('hello') и 5 байт совпадают.

Понравился урок? Сошлитесь на него

«Компьютер не хранит буквы — он хранит числа.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.