От чисел к данным: кодировки и адреса
Внутри компьютера нет ни букв, ни картинок, ни адресов сайтов — только числа. Разберёмся, как одни и те же числа превращаются то в символы текста, то в адрес в сети.
Кодировка — это таблица соответствия «число ↔ символ»: каждому символу присвоен номер (код), и текст в памяти хранится как последовательность этих номеров, записанных в двоичной системе.
В прошлых уроках мы видели, что байты можно толковать как цвет или как набор флагов. Но самое частое применение чисел — это текст. Когда ты набираешь сообщение, компьютер не хранит «буквы»: он хранит их номера. Какая буква под каким номером — определяет кодировка. Понять её — значит понять, как системы счисления связывают сырые числа с осмысленными данными.
Как числа становятся символами
Исторически первой массовой таблицей стала ASCII. Она присваивает код каждому символу английского алфавита, цифрам и знакам препинания. Кодов всего 128, то есть номера от 0 до 127 — это ровно 7 бит, потому что $2^7 = 128$. Несколько опорных значений стоит запомнить:
| Символ | Код (10) | Код (16) | Код (2) |
|---|---|---|---|
| пробел | 32 | 20 | 0100000 |
| '0' | 48 | 30 | 0110000 |
| 'A' | 65 | 41 | 1000001 |
| 'a' | 97 | 61 | 1100001 |
Заметь закономерность: цифры идут подряд начиная с 48, заглавные буквы — с 65, строчные — с 97. Поэтому буква 'C' имеет код $65 + 2 = 67$, а между 'A' и 'a' ровно $97 - 65 = 32$ — это пригодится для смены регистра.
ord и chr в Python
В Python есть две парные функции: ord(символ) возвращает номер символа, а chr(номер) — наоборот, символ по номеру. Это прямой мост между числом и буквой. Посмотрим:
# символ -> номер
print("Код 'A':", ord("A"))
print("Код 'a':", ord("a"))
print("Код '0':", ord("0"))
# номер -> символ
print("chr(66):", chr(66))
print("chr(97):", chr(97))
# слово как список кодов
word = "Hi!"
codes = [ord(c) for c in word]
print("Коды слова Hi!:", codes)Вывод:
Код 'A': 65 Код 'a': 97 Код '0': 48 chr(66): B chr(97): a Коды слова Hi!: [72, 105, 33]
Слово Hi! превратилось в три числа: 72, 105, 33. Именно так оно и лежит в памяти — как байты со значениями этих кодов. Раз код заглавной отличается от строчной ровно на 32, перевести букву в нижний регистр можно арифметикой:
letter = "H"
lower = chr(ord(letter) + 32)
print("H в нижнем регистре:", lower)Вывод:
H в нижнем регистре: h
От ASCII к Unicode
128 символов хватает для английского, но в мире есть кириллица, иероглифы, эмодзи. Поэтому появился Unicode — огромная таблица, где номер (его называют «кодовая точка») есть у каждого символа любого языка. Русская 'А' имеет код 1040, а 'Я' — 1071. Тот же ord работает и с ними:
$$ \text{ord}(\text{'А'}) = 1040, \qquad \text{ord}(\text{'Я'}) = 1040 + 31 = 1071 $$
ASCII при этом не выбросили: первые 128 кодов Unicode совпадают с ASCII один в один. Поэтому ord("A") и там, и там равно 65 — старые тексты остаются читаемыми. Меняется лишь то, что под большие коды нужно больше битов: одного байта (максимум 255) на номер 1040 уже не хватает, и кодировки вроде UTF-8 хранят такой символ несколькими байтами.
Как это работает: IP-адрес как 4 байта
Числа становятся не только текстом. Возьми адрес сайта в сети — например, 192.168.1.10. Это формат IPv4, и за точками скрываются ровно 4 байта. Каждое число между точками — один байт, поэтому каждое из них лежит в диапазоне 0–255 (вспомни: байт хранит как раз 0–255). Адрес 192.168.1.10 — это четыре байта со значениями 192, 168, 1 и 10.
Сколько всего адресов даёт такая схема? Четыре байта — это 32 бита, значит:
$$ 256^4 = 2^{32} = 4\,294\,967\,296 $$
Около 4,3 миллиарда — и этого, кстати, уже не хватает на все устройства планеты, поэтому придумали IPv6 с 128 битами. Весь адрес можно свернуть в одно большое число, разложив байты по разрядам системы с основанием 256:
parts = [192, 168, 1, 10]
# собираем 4 байта в одно 32-битное число
value = 0
for p in parts:
value = value * 256 + p
print("Адрес как число:", value)
print("В шестнадцатеричной:", hex(value))
# и обратно: достаём байты делением с остатком
back = []
n = value
for _ in range(4):
back.append(n % 256)
n = n // 256
back.reverse()
print("Обратно в байты:", back)Вывод:
Адрес как число: 3232235786 В шестнадцатеричной: 0xc0a8010a Обратно в байты: [192, 168, 1, 10]
Видишь связь? Чтобы собрать байты в число, мы умножаем на 256 и прибавляем следующий — это та же позиционная запись, что и обычное число, только основание не 10, а 256. А чтобы разобрать обратно, делим с остатком на 256. Те же действия, что при переводе между системами счисления — просто разряд тут размером в байт. И заметь: в hex адрес 0xc0a8010a распадается на пары c0 a8 01 0a, и это ровно 192, 168, 1, 10 — снова видно, что байт = две hex-цифры.
Частые ошибки
- Путать символ-цифру и само число.
ord("0")равно 48, а не 0. Символ'0'— это просто значок с кодом 48, а не число ноль. - Думать, что ASCII покрывает кириллицу. Русские буквы — это уже Unicode (коды больше 127), в чистый ASCII они не помещаются.
- Превышать диапазон байта в IP. Записи вроде
192.300.1.1не бывает: 300 больше 255 и в один байт не влезает. - Считать, что один символ — всегда один байт. В UTF-8 английская буква занимает 1 байт, а русская или эмодзи — несколько. Число символов и число байтов могут не совпадать.
Итоги
- Текст в памяти — это коды символов; кодировка задаёт таблицу «число ↔ символ».
- ASCII покрывает 128 символов (7 бит, $2^7$); цифры идут с 48, заглавные с 65, строчные с 97.
ordдаёт код символа,chr— символ по коду; разница регистров латиницы ровно 32.- Unicode расширяет таблицу до всех языков, сохраняя первые 128 кодов совместимыми с ASCII.
- IPv4-адрес — это 4 байта (числа 0–255), всего $2^{32} \approx 4{,}3$ млрд адресов; сборка и разбор — позиционная запись по основанию 256.