Символы и текст: от ASCII до UTF-8
Урок объясняет, как текст становится числами: от 7-битного ASCII до универсального Unicode и его кодировки UTF-8.
Кодировка символов — соглашение, сопоставляющее каждому символу число (кодовую точку), а затем эти числа — последовательностям байтов для хранения и передачи.
Зачем стандартизировать буквы
Компьютер хранит только числа, поэтому буква «A» — это тоже число. Но если ваш компьютер считает «A» = 65, а мой — = 200, мы не сможем обмениваться текстом. Нужна общая таблица. Первым широким стандартом стал ASCII: 128 символов (латиница, цифры, знаки, управляющие коды), каждый в 7 битах.
Таблица ASCII — ключевые диапазоны
| Коды | Что это |
| 0–31 | управляющие (перевод строки, табуляция) |
| 48–57 | цифры '0'–'9' |
| 65–90 | заглавные 'A'–'Z' |
| 97–122 | строчные 'a'–'z' |
Заметьте красивое свойство: разница между заглавной и строчной буквой ровно 32 (= 2^5), то есть один бит. Поэтому смена регистра — это переключение одного бита.
for ch in "Aa0 Z":
print(f"'{ch}' -> код {ord(ch):>3} -> двоичное {ord(ch):08b}")
print("Разница 'a' и 'A':", ord('a') - ord('A'))Вывод:
'A' -> код 65 -> двоичное 01000001 'a' -> код 97 -> двоичное 01100001 '0' -> код 48 -> двоичное 00110000 ' ' -> код 32 -> двоичное 00100000 'Z' -> код 90 -> двоичное 01011010 Разница 'a' и 'A': 32
Проблема: мир больше латиницы
128 символов хватает только для английского. Кириллица, иероглифы, эмодзи — десятки тысяч знаков. Появилось множество несовместимых 8-битных кодировок (KOI8-R, Windows-1251, и т.д.), отсюда «кракозябры», когда текст читали не в той кодировке. Решением стал Unicode — единый реестр, где каждому символу мира присвоена уникальная кодовая точка (сейчас их более 140 000).
Как работает под капотом: UTF-8
Unicode — это только номера. Как записать номер 128512 (эмодзи) в байты? Самый популярный ответ — UTF-8: переменная длина 1–4 байта. ASCII-символы остаются 1 байтом (обратная совместимость!), а более «дальние» символы занимают 2–4 байта. Старшие биты первого байта говорят, сколько байт в символе.
Диапазон кодовой точки | Шаблон байтов UTF-8 0x00 .. 0x7F (ASCII) | 0xxxxxxx (1 байт) 0x80 .. 0x7FF | 110xxxxx 10xxxxxx (2 байта) 0x800 .. 0xFFFF | 1110xxxx 10xxxxxx 10xxxxxx (3 байта) 0x10000 .. 0x10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
for ch in ["A", "Я", "中", "🐣"]:
raw = ch.encode("utf-8")
hexbytes = " ".join(f"{b:02X}" for b in raw)
print(f"'{ch}' код U+{ord(ch):04X} -> {len(raw)} байт: {hexbytes}")Вывод:
'A' код U+0041 -> 1 байт: 41 'Я' код U+042F -> 2 байт: D0 AF '中' код U+4E2D -> 3 байт: E4 B8 AD '🐣' код U+1F423 -> 4 байт: F0 9F 90 A3
Историческая справка: эпоха «кракозябр» и почему её победили
Промежуток между ASCII и Unicode был эпохой настоящего хаоса, особенно болезненной для кириллицы. Поскольку ASCII занимал лишь нижние 128 кодов, верхнюю половину байта (коды 128–255) каждый народ заполнял по-своему. Только для русского языка одновременно жили KOI8-R, Windows-1251, CP866 (DOS), ISO 8859-5 и кодировка Apple — и все несовместимые. Письмо, набранное в одной кодировке и прочитанное в другой, превращалось в знаменитые «кракозябры»: «Привет» становилось «Ïðèâåò». Веб-страницы выпадали с меню «выбора кодировки», а почтовые программы славились искажением писем. Это была не редкая неприятность, а ежедневная реальность 1990-х.
Unicode возник именно как радикальное решение этого хаоса: вместо того чтобы делить тесные 128 кодов между всеми языками мира, выделить каждому символу планеты — латинскому, кириллическому, китайскому иероглифу, эмодзи — свой уникальный, навсегда закреплённый номер. Сегодня «кракозябры» почти исчезли не потому, что кодировки стали умнее, а потому что мир сошёлся на одной общей таблице и одном доминирующем способе её записи — UTF-8.
Глубже: чем гениален дизайн UTF-8
UTF-8, придуманный Кеном Томпсоном и Робом Пайком в 1992 году буквально за ужином, обладает несколькими неочевидными, но блестящими свойствами. Во-первых, обратная совместимость: любой старый ASCII-файл уже является корректным UTF-8 без единого изменения, ведь коды 0–127 кодируются одним байтом, как и раньше. Во-вторых, самосинхронизация: по любому байту видно, начало это символа или его продолжение, потому что байты-продолжения всегда имеют префикс 10. Если поток данных оборвался или повредился посередине, декодер не «съезжает» навсегда, а находит начало следующего символа и продолжает — критично для сетевой передачи.
В-третьих, UTF-8 не содержит нулевых байтов внутри многобайтных символов, поэтому старый код на C, считающий конец строки по нулю, не ломается. И наконец, порядок байтов сохраняет естественную сортировку кодовых точек. Эти свойства — не везение, а продуманный инженерный дизайн, и именно поэтому UTF-8 вытеснил конкурента UTF-16 почти всюду в вебе и файлах.
Аналогия и подводный камень: символ ≠ байт ≠ «то, что видит глаз»
Переменная длина UTF-8 создаёт ловушку, на которую попадаются даже опытные программисты. Длина строки в байтах, в кодовых точках и в видимых глазу символах — это три разные величины. Эмодзи семьи может состоять из нескольких кодовых точек (отдельные человечки), склеенных невидимым «соединителем», и занимать десяток байт, оставаясь одним значком на экране. Поэтому «обрезать строку до 10 символов», тупо взяв первые 10 байт, — верный способ разрубить символ посередине и получить «кракозябру» на стыке. Эта же логика объясняет, почему счётчик символов в Твиттере и реальная длина строки в памяти — совсем не одно и то же, и почему корректная работа с текстом всегда требует помнить, на каком из трёх уровней — байты, кодовые точки или графемы — вы сейчас считаете.
Частые ошибки
- Путать символ и байт. В UTF-8 один символ может занимать несколько байтов; длина строки в символах != длина в байтах.
- Читать файл не в той кодировке. Отсюда «кракозябры»; всегда указывайте
encoding="utf-8". - Думать, что Unicode = UTF-8. Unicode — это таблица номеров; UTF-8/UTF-16 — способы записать эти номера в байты.
Итог
- ASCII: 128 символов в 7 битах; регистр буквы — это один бит (разница 32).
- Unicode даёт каждому символу мира уникальный номер (кодовую точку).
- UTF-8 кодирует кодовые точки 1–4 байтами и совместим с ASCII.