Unicode и UTF-8: как работает кодировка
История Unicode, UTF-8 vs UTF-16, BOM, кодирование кириллицы, эмодзи, проблемы кодировок.
Введение
Если вы когда-нибудь видели «кракозябры» вместо русских букв на веб-странице или сталкивались с mysterious «?» в сохранённом файле, — вы стали жертвой путаницы с кодировками. Unicode и UTF-8 — это два связанных понятия, которые наконец-то решили проблему представления текста в компьютерах. В этой статье подробно разберём, что такое Unicode, чем UTF-8 отличается от UTF-16 и UTF-32, как кодируется кириллица и эмодзи, что такое BOM и почему UTF-8 победил все остальные кодировки.
Попрактиковаться в конвертации между кодировками можно в нашем инструменте Unicode конвертер.
Что такое Unicode
Unicode — это международный стандарт кодирования символов (ISO/IEC 10646), который ставит целью присвоить уникальный номер каждому символу всех письменностей мира, а также математическим знакам, техническим символам и даже эмодзи. Стандарт разрабатывается консорциумом Unicode с 1991 года и сегодня содержит более 150 000 символов.
Каждый символ в Unicode имеет уникальный номер, называемый code point(кодовой позицией). Code points записываются в виде U+XXXX, где XXXX — шестнадцатеричное число. Например:
U+0041— латинская «A»U+0410— русская «А»U+0430— русская «а»U+20BD— знак рубля «₽»U+1F600— эмодзи 😀
Code points organised в 17 плоскостях (planes) по 65 536 символов в каждой. Первая плоскость (BMP — Basic Multilingual Plane) содержит символы всех современных письменностей, и именно её покрывает большинство реализаций. Остальные плоскости заняты историческими письменностями, эмодзи, музыкальными символами и зарезервированы.
Unicode — это не кодировка
Важное различие: Unicode определяет, какие номера присвоены символам, но не определяет, как именно эти номера хранятся в памяти или передаются по сети. За это отвечают формы кодирования (Unicode Transformation Format, UTF): UTF-8, UTF-16 и UTF-32. Они различаются размером минимальной единицы и способом упаковки code points в байты.
UTF-8: как устроена
UTF-8 — самая популярная кодировка Unicode, и не случайно. Она переменной длины: каждый символ занимает от 1 до 4 байтов. При этом символы ASCII (U+0000–U+007F) занимают ровно 1 байт и совпадают с классическим ASCII побайтно. Это делает UTF-8 обратно совместимой со всеми старыми программами, рассчитанными на ASCII.
| Диапазон code points | Байтов | Шаблон |
|---|---|---|
| U+0000 – U+007F | 1 | 0xxxxxxx |
| U+0080 – U+07FF | 2 | 110xxxxx 10xxxxxx |
| U+0800 – U+FFFF | 3 | 1110xxxx 10xxxxxx 10xxxxxx |
| U+10000 – U+10FFFF | 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
Старшие биты первого байта указывают, сколько байтов в последовательности. Все продолжающие байты начинаются с «10» — это позволяет декодеру легко отличить начало символа от продолжения и восстановиться после ошибок.
Пример: кодируем русскую «А»
Code point «А» — U+0410, то есть 1040 в десятичной системе. В двоичном виде:00000100 00010000. Это попадает в диапазон U+0080–U+07FF, значит, нужно 2 байта.
Code point: U+0410 = 0000 0100 0001 0000 (11 бит значащих)
Разбиваем на 5+6 бит: 00100 000100
Шаблон: 110xxxxx 10xxxxxx
110 00100 10 000100
= 11010000 10010000
= 0xD0 0x90В файле UTF-8 буква «А» занимает два байта: 0xD0 0x90. Все русские буквы кодируются двумя байтами.
Пример: кодируем эмодзи «😀»
Эмодзи попадают в диапазон U+10000–U+10FFFF, для них нужно 4 байта. Например, «😀» имеет code point U+1F600.
U+1F600 = 00001111 01100000 00000000 (21 бит значащих)
Разбиваем: 000 011110 110000 000000
Шаблон: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
11110 000 10 011110 10 110000 10 000000
= 11110000 10011110 10011000 10000000
= 0xF0 0x9F 0x98 0x80Эмодзи «😀» занимает 4 байта: 0xF0 0x9F 0x98 0x80.
UTF-16 и UTF-32
UTF-16
Использует 2 байта для символов BMP и 4 байта (две суррогатные пары) для символов вне BMP. Применяется в Windows API, JavaScript (строки внутри V8 и других движков хранятся как UTF-16), Java. Главное неудобство — переменная длина: нельзя просто проиндексировать строку по номеру символа, нужно учитывать суррогатные пары.
UTF-32
Каждый символ занимает ровно 4 байта. Самая простая для обработки (можно индексировать напрямую), но самая расточительная: текст на английском увеличивается в 4 раза по сравнению с ASCII, на русском — в 2 раза по сравнению с UTF-8. На практике почти не используется.
Сравнение UTF-8, UTF-16 и UTF-32
| Свойство | UTF-8 | UTF-16 | UTF-32 |
|---|---|---|---|
| Минимальный размер символа | 1 байт | 2 байта | 4 байта |
| Максимальный размер символа | 4 байта | 4 байта | 4 байта |
| Совместимость с ASCII | Да | Нет | Нет |
| Размер текста на английском | 1 байт/символ | 2 байт/символ | 4 байт/символ |
| Размер текста на русском | 2 байт/символ | 2 байт/символ | 4 байт/символ |
| Размер текста с эмодзи | 4 байт/символ | 4 байт/символ | 4 байт/символ |
| Порядок байтов | Не важен | Важен (LE/BE) | Важен (LE/BE) |
| Где используется | Web, Linux, JSON | Windows, Java, JS | Редко |
UTF-8 универсально выигрывает для текста на латинице и почти не проигрывает для русского текста (по 2 байта на символ, как в UTF-16). Для китайского или японского UTF-16 иногда оказывается компактнее, но разница невелика.
BOM: byte order mark
Для UTF-16 и UTF-32 важен порядок байтов: little-endian (младший байт первым) или big-endian (старший первым). Чтобы потребитель данных знал, какой порядок использовать, в начало файла иногда помещают специальный символ U+FEFF — BOM (Byte Order Mark). В UTF-16LE он записывается как 0xFF 0xFE, в UTF-16BE — как0xFE 0xFF.
Для UTF-8 BOM не нужен (порядок байтов фиксирован), но иногда в начало файла всё равно помещают последовательность 0xEF 0xBB 0xBF. Это часто вызывает проблемы: некоторые парсеры (особенно PHP) воспринимают BOM как обычный текст и выводят его в начало документа, ломая HTTP-заголовки или XML-декларацию. Поэтому в UTF-8 BOM обычно не используют.
Проблемы с кодировками
1. «Кракозябры» при открытии файла
Если файл сохранён в UTF-8, а открыт как Windows-1251, русские буквы превратятся в бессмысленный набор символов вроде «РџСЂРёРІРµС‚». Это потому, что две байта UTF-8 интерпретируются как два отдельных символа Windows-1251. Решение — правильно указать кодировку при открытии.
2. Знаки вопроса «???»
Если попытаться сохранить символ, не входящий в целевую кодировку, он заменяется на «?». Например, при сохранении текста с эмодзи в Windows-1251 все эмодзи превратятся в вопросы. Это необратимая потеря данных.
3. Разная длина строк
В UTF-16 количество 16-битных «единиц» не равно количеству символов, если есть эмодзи или редкие символы (суррогатные пары). В JavaScript это приводит к сюрпризам:
'😀'.length // 2 (суррогатная пара)
[...'😀'].length // 1 (правильно через spread)
'😀'.codePointAt(0) // 128512 (1F600)
'😀'.charCodeAt(0) // 55357 (только первый суррогат)4. Нормализация
Один и тот же визуальный символ может быть представлен разными последовательностями code points. Например, «é» можно записать как один символ U+00E9 или как комбинацию «e» (U+0065) и объединяющего знака ударения (U+0301). Визуально они идентичны, но побайтово различаются. Это создаёт проблемы при сравнении строк. Решение — нормализация (NFC, NFD, NFKC, NFKD).
'é'.normalize('NFC') === 'e\u0301'.normalize('NFC') // trueUTF-8 и веб
Стандарт HTML5 требует указывать кодировку UTF-8 в первых 1024 байтах документа:
<meta charset="utf-8">Современные браузеры по умолчанию используют UTF-8, но лучше объявить его явно. То же касается HTTP-заголовка Content-Type: text/html; charset=utf-8 — он имеет приоритет над meta-тегом.
JSON по стандарту обязан использовать UTF-8 (RFC 8259). XML-декларация<?xml version="1.0" encoding="UTF-8"?> — обязательна, если кодировка отличается от UTF-8, и опциональна для UTF-8.
Работа с Unicode в разных языках
JavaScript
// Code point в символ
String.fromCodePoint(0x1F600); // "😀"
// Символ в code point
'😀'.codePointAt(0); // 128512
// Безопасная длина строки с эмодзи
[...'Привет 😀'].length; // 8, а не 9
// Экранирование в строках
'\u{1F600}'; // "😀" (ES6+)Python
# Python 3: строки — это Unicode
s = 'Привет 😀'
print(len(s)) # 8
# Кодировка в байты UTF-8
b = s.encode('utf-8')
print(b) # b'\xd0\x9f\xd1\x80...\xf0\x9f\x98\x80'
# Декодирование обратно
s2 = b.decode('utf-8')
# Code point
ord('А') # 1040
chr(0x1F600) # '😀'PHP
<?php
// Стандартные строковые функции не понимают многобайтовые кодировки
// Используйте mb_* аналоги:
mb_strlen('Привет', 'UTF-8'); // 6
mb_strtoupper('Привет', 'UTF-8'); // "ПРИВЕТ"
mb_substr('Привет, мир!', 8, 3, 'UTF-8'); // "мир"Эмодзи и составные символы
Современные эмодзи бывают разными. Простые — это один code point (😀 = U+1F600). Но многие эмодзи — это последовательности:
- С модификатором цвета кожи: 👍🏽 = U+1F44D (палец вверх) + U+1F3FD (средний тон кожи).
- С нулевым соединителем (ZWJ): 👨👩👧 = три эмодзи, соединённых символом U+200D (Zero Width Joinner). Визуально один символ, фактически пять code points.
- С флагами: 🇷🇺 = U+1F1F7 (региональный индикатор R) + U+1F1FA (региональный индикатор U).
Всё это делает обработку эмодзи нетривиальной: подсчёт «символов» в строке с эмодзи может дать разные результаты в зависимости от того, что вы считаете — code points, code units или графемные кластеры.
Почему UTF-8 победил
Когда в 1990-х годах обсуждался переход на Unicode, многие считали, что стандартом станет UTF-16. Но в итоге UTF-8 взял верх по нескольким причинам:
- Обратная совместимость с ASCII. Любой ASCII-текст — это валидный UTF-8 текст. Старые программы продолжают работать.
- Компактность для европейских языков. Английский текст весит столько же, сколько в ASCII. Русский — 2 байта на символ, как в UTF-16.
- Нет проблем с порядком байтов. Не нужен BOM, нет LE/BE вариантов.
- Устойчивость к ошибкам. Если один байт потерян, теряется только один символ, а не весь последующий текст.
- Поддержка в веб-стандартах. HTML, JSON, XML, HTTP — везде UTF-8 стал стандартом по умолчанию.
Сегодня UTF-8 использует более 98% всех веб-страниц в мире. Историю того, как мы к этому пришли — от ASCII через Windows-1251 к UTF-8, — читайте в нашей статье об истории кодировок.
Заключение
Unicode — это огромная таблица, в которой каждому символу всех письменностей мира присвоен уникальный номер. UTF-8 — это самая популярная и удобная форма кодирования Unicode: она обратно совместима с ASCII, компактна для европейских языков и устойчива к ошибкам. Если вы разрабатываете веб-приложение, используйте UTF-8 везде — от базы данных до HTTP-заголовков. Это убережёт вас и ваших пользователей от десятков неприятных проблем с кодировками.
Для практической работы с Unicode — конвертации символов в code points и обратно, просмотра байтового представления строки — используйте наш Unicode конвертер. А если хотите лучше понять, как символы превращаются в байты, почитайте статью о двоичном кодировании текста.
Попробуйте эти инструменты
Похожие статьи
Base64 — что это и как работает
Принцип кодирования Base64, алфавит, padding, использование в Data URI, email, API. Примеры кодирования.
URL кодирование: percent-encoding explained
Что такое URL encoding, зарезервированные символы, как кодировать/декодировать URL, частые ошибки.
HTML сущности и кодирование спецсимволов
HTML entities, named vs numeric, XSS защита, кодирование кавычек, амперсандов, угловых скобок.
JWT токен: структура и как декодировать
JSON Web Token: header, payload, signature. Как работает аутентификация JWT, безопасность, декодирование.