Справочник · Данные и форматы
Кодировки и UTF-8: почему текст превращается в кракозябры
Чем символ отличается от байта, как устроен UTF-8, откуда берутся «Ð¿Ñ€Ð¸Ð²ÐµÑ‚» и знаки вопроса и как перекодировать файлы.
Компьютер хранит байты, а не буквы. Кодировка — это правило, по которому байты превращаются в символы и обратно. Если записали текст в одной кодировке, а прочитали в другой, получаются кракозябры. Сегодня почти везде используется UTF-8, но старые файлы, Windows-программы и Excel до сих пор подкидывают сюрпризы.
Символы, кодовые точки и байты
Юникод — это таблица, где каждому символу присвоен номер, кодовая точка. Записывается как U+ и шестнадцатеричное число: латинская A — U+0041, русская Я — U+042F, знак евро — U+20AC, эмодзи с улыбкой — U+1F600. Всего точек больше миллиона, от U+0000 до U+10FFFF.
Как номер превратить в байты, решает кодировка:
| Кодировка | Сколько байт на символ | Где встречается |
|---|---|---|
| ASCII | 1, только 128 символов | основа всех остальных |
| Windows-1251 (cp1251) | 1, кириллица и латиница | старые файлы Windows, 1С, Excel |
| KOI8-R | 1 | старые письма и Unix-системы |
| CP866 | 1 | консоль Windows, DOS |
| UTF-8 | от 1 до 4 | веб, Linux, macOS, git, почти всё новое |
| UTF-16 | 2 или 4 | внутри Windows, Java, JavaScript, .NET |
| UTF-32 | всегда 4 | редко, во внутренних структурах |
Как устроен UTF-8
Чем больше номер символа, тем больше байт. По первым битам байта сразу видно, сколько байт в символе.
| Диапазон кодовых точек | Байты в двоичном виде | Байт |
|---|---|---|
U+0000–U+007F | 0xxxxxxx | 1 |
U+0080–U+07FF | 110xxxxx 10xxxxxx | 2 |
U+0800–U+FFFF | 1110xxxx 10xxxxxx 10xxxxxx | 3 |
U+10000–U+10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 4 |
Пример: буква Я — это U+042F, двоичное 100 0010 1111. Она попадает во второй диапазон. Раскладываем 11 бит по шаблону 110xxxxx 10xxxxxx: получаются байты 11010000 10101111, то есть D0 AF.
Что важно знать:
- ASCII-текст в UTF-8 не меняется ни на байт. Поэтому код, конфиги и протоколы спокойно переходят на UTF-8.
- Кириллица занимает 2 байта, большинство иероглифов — 3, эмодзи — 4.
- Продолжающие байты всегда начинаются с
10, поэтому по любому месту в потоке можно найти начало символа. - Не всякая последовательность байт — правильный UTF-8. Например, одиночный
0xD0в конце файла — ошибка.
printf 'Я' | xxd # 00000000: d0af
printf 'Я' | wc -c # 2 байта
printf 'Я' | wc -m # 1 символ (если локаль UTF-8)Длина строки — понятие неоднозначное
Строка "ёж👍" в разных языках имеет разную «длину»:
| Где считаем | Результат | Что считается |
|---|---|---|
| байты в UTF-8 | 8 | 2 + 2 + 4 |
JavaScript .length | 4 | единицы UTF-16: эмодзи занимает две |
Python len() | 3 | кодовые точки |
| то, что видит человек | 3 | графемы |
А буква ё может быть записана и одной кодовой точкой U+0451, и двумя: е плюс комбинируемая диакритика U+0308. На экране разницы нет, но строки не равны, и поиск по ним не совпадёт. Это решает нормализация: форма NFC собирает символы в составные, NFD раскладывает.
"ё".normalize("NFD").length → 2
[..."ёж👍"].length → 3, перебор по кодовым точкам
new TextEncoder().encode("ёж👍").length → 8 байтimport unicodedata
unicodedata.normalize("NFC", s) == unicodedata.normalize("NFC", t)Узнаём кракозябры по виду
По характерному мусору часто сразу понятно, что произошло.
| Что видите вместо «привет» | Что случилось |
|---|---|
привет | UTF-8 прочитали как Latin-1 или Windows-1252 |
привет | UTF-8 прочитали как Windows-1251 |
ïðèâåò | Windows-1251 прочитали как Latin-1 |
������ или привет с ромбиками | байты не являются правильным UTF-8, программа заменила их на U+FFFD |
?????? | при перекодировании символов не нашлось в целевой кодировке, они потеряны |
Первые три случая обратимы: байты целы, их просто неправильно прочитали. Последние два — нет, исходные данные уже испорчены.
BOM
Метка порядка байтов (BOM) — символ U+FEFF в начале файла. В UTF-8 это байты EF BB BF. Для UTF-8 она не нужна, но Блокнот Windows и Excel любят её ставить и ожидать.
- Скрипт с BOM в начале не запустится: строка
#!/bin/bashперестаёт быть первой. - JSON с BOM многие парсеры отвергают.
- Excel, наоборот, без BOM откроет CSV в UTF-8 как Windows-1251. Подробнее — в статье о CSV.
head -c 3 file.txt | xxd # ef bb bf — значит BOM есть
sed -i '1s/^\xEF\xBB\xBF\(.*\)/\1/' file.txt # убрать BOMПерекодирование файлов
file -i report.txt # угадать кодировку
iconv -f WINDOWS-1251 -t UTF-8 old.txt > new.txt
iconv -c -f UTF-8 -t WINDOWS-1251 in.txt > out.txt # -c: непереводимые символы выбросить
iconv -l | grep -i 1251 # как называется кодировкаВ Python кодировку всегда стоит указывать явно, иначе на Windows подставится системная:
with open("data.txt", encoding="utf-8") as f:
text = f.read()
raw.decode("cp1251") # байты из Windows-1251
text.encode("utf-8", errors="strict")
"привет".encode("cp1252").decode("utf-8") # починка первого вида кракозябрКодировка в вебе и базе данных
- HTML:
<meta charset="utf-8">в первых 1024 байтах страницы, а лучше ещё и заголовокContent-Type: text/html; charset=utf-8. - В адресах не-ASCII символы кодируются как байты UTF-8 в виде
%D0%AF. Посмотреть и перевести можно в кодировщике URL. - MySQL: используйте
utf8mb4. Стараяutf8в MySQL хранит максимум 3 байта на символ и не принимает эмодзи. - PostgreSQL: кодировка задаётся при создании базы,
ENCODING 'UTF8'. - Base64 работает с байтами, поэтому перед кодированием текста его всё равно переводят в UTF-8. Наш инструмент Base64 делает это сам.
Короткие правила
- Везде UTF-8 без BOM, если нет причины иначе.
- Указывайте кодировку явно при чтении и записи файлов.
- Считайте длину в тех единицах, которые важны: байты для лимитов хранения, графемы для интерфейса.
- Сравнивайте пользовательские строки после нормализации NFC.
- Встретили кракозябры — сначала сохраните исходные байты, потом экспериментируйте.