Символы и текст: от 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.
Проверьте себя
1. Чему равна разница между кодами 'a' и 'A' в ASCII?
A1
B16
C32
D64
2. В чём разница между Unicode и UTF-8?
AЭто синонимы
BUnicode — таблица номеров символов, UTF-8 — способ записать номера в байты
CUTF-8 поддерживает больше символов, чем Unicode
DUnicode — для веба, UTF-8 — для файлов
3. Сколько байтов занимает обычный латинский символ ASCII в UTF-8?
A1
B2
C3
D4