Юникод: 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–127 | ASCII: латиница, цифры, знаки | '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')))
1. Что вернёт ord('A')?
2. Сколько байтов займёт 'мир' в utf-8?
3. Что такое кракозябры в файле?
4. Как прочитать файл старой российской системы с кириллицей?
5. Что вернёт chr(ord('b') - 1)?
6. Что вернёт ord('А')?
7. Зачем нужны ord и chr, если есть готовые методы строк?
8. Что вернёт len('привет'.encode('utf-8'))?
Для слова 'yes' напечатайте код первой буквы через ord, соберите слово 'no' из кодов 110 и 111 через chr, и напечатайте длину в байтах строки 'привет' в utf-8.
Что лучше: строки или байты в 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-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Строки · Урок 13
Экранирование и сырые строки
Как напечатать кавычку внутри кавычек и путь Windows, который не ломается? Знакомимся с обратным слэшем — символом, который меняет смысл следующего символа.
Строки · Урок 15
count: считаем вхождения в строке
count отвечает на вопрос «сколько?»: вхождений подстроки, символов в слове, слов в отзыве. Разбираем непересекающийся подсчёт, диапазон поиска и ловушку строки 'ааа'.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
requests · Урок 4
Ответ requests: text, content и json — три лица ответа
У ответа requests три лица: .text — строка, .content — байты, .json() — готовый объект Python. Разбираемся, когда какое брать и откуда берутся кракозябры.
requests json decode errorutf-8 cp1251 python
json · Урок 13
Что отдаёт API: ответ как текст
API отвечает текстом со статусом и заголовками: берём заранее сохранённый настоящий ответ — и учимся мерить его длину, искать подстроки и превращать байты в строку.
python байты ответа decode utf-8
Строки · Урок 1
Строки в Python: текст как данные
Строка — самый частый тип данных в реальных задачах: имена, города, сообщения. Знакомимся с типом str: кавычки, len, конкатенация и умножение — и узнаём, почему строку нельзя переписать на месте.
python строкистроки в python