Все статьи
Кодирование

Unicode и UTF-8: как работает кодировка

История Unicode, UTF-8 vs UTF-16, BOM, кодирование кириллицы, эмодзи, проблемы кодировок.

18 февраля 2025
10 мин чтения
ConvertHub
#unicode#utf-8#кодировка

Введение

Если вы когда-нибудь видели «кракозябры» вместо русских букв на веб-странице или сталкивались с 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+007F10xxxxxxx
U+0080 – U+07FF2110xxxxx 10xxxxxx
U+0800 – U+FFFF31110xxxx 10xxxxxx 10xxxxxx
U+10000 – U+10FFFF411110xxx 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-8UTF-16UTF-32
Минимальный размер символа1 байт2 байта4 байта
Максимальный размер символа4 байта4 байта4 байта
Совместимость с ASCIIДаНетНет
Размер текста на английском1 байт/символ2 байт/символ4 байт/символ
Размер текста на русском2 байт/символ2 байт/символ4 байт/символ
Размер текста с эмодзи4 байт/символ4 байт/символ4 байт/символ
Порядок байтовНе важенВажен (LE/BE)Важен (LE/BE)
Где используетсяWeb, Linux, JSONWindows, 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') // true

UTF-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 конвертер. А если хотите лучше понять, как символы превращаются в байты, почитайте статью о двоичном кодировании текста.

Попробуйте эти инструменты

Похожие статьи