UTF-8
UTF-8 (от англ. Unicode Transformation Format, 8-bit — «формат преобразования Юникода, 8-бит») — распространённый стандарт кодирования символов, позволяющий более компактно хранить и передавать символы Юникода, используя переменное количество байт (от 1 до 4), и обеспечивающий полную обратную совместимость с 7-битной кодировкой ASCII. Стандарт UTF-8 официально закреплён в документах RFC 3629 и ISO/IEC 10646 Annex D.
Кодировка UTF-8 сейчас является доминирующей в веб-пространстве. Она также нашла широкое применение в UNIX-подобных операционных системах[1].
Формат UTF-8 был разработан 2 сентября 1992 года Кеном Томпсоном и Робом Пайком, и реализован в Plan 9[2]. Идентификатор кодировки в Windows — 65001[3].
UTF-8, по сравнению с UTF-16, наибольший выигрыш в компактности даёт для текстов на латинице, поскольку латинские буквы без диакритических знаков, цифры и наиболее распространённые знаки препинания кодируются в UTF-8 лишь одним байтом, и коды этих символов соответствуют их кодам в ASCII.[4][5]
Алгоритм кодирования (сериализации)
[править | править код]Алгоритм кодирования в UTF-8 стандартизирован в RFC 3629 и состоит из 3 этапов:
1. Определить количество октетов, требуемых для кодирования символа. Номер символа берётся из стандарта Юникода.
| Диапазон номеров символов | Требуемое количество октетов |
|---|---|
00000000-0000007F
|
1 |
00000080-000007FF
|
2 |
00000800-0000FFFF
|
3 |
00010000-0010FFFF
|
4 |
Для символов Юникода с номерами от U+0000 до U+007F (занимающими один октет c нулём в старшем бите) кодировка UTF-8 полностью соответствует 7-битной кодировке US-ASCII.
2. Установить старшие биты первого октета в соответствии с необходимым количеством октетов для кодирования символа, определённом на первом этапе:
- 0xxxxxxx — если для кодирования потребуется один октет;
- 110xxxxx — если для кодирования потребуется два октета;
- 1110xxxx — если для кодирования потребуется три октета;
- 11110xxx — если для кодирования потребуется четыре октета.
Если для кодирования требуется больше одного октета, то в октетах 2-4 два старших бита всегда устанавливаются равными 102 (10xxxxxx). Это позволяет легко отличать первый октет в потоке, от других октетов
| Количество октетов | Значащих бит | Шаблон |
|---|---|---|
| 1 | 7 | 0xxxxxxx
|
| 2 | 11 | 110xxxxx 10xxxxxx
|
| 3 | 16 | 1110xxxx 10xxxxxx 10xxxxxx
|
| 4 | 21 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
|
3. Установить значащие биты октетов в соответствии с номером символа Юникода, выраженном в двоичном виде. Начать заполнение с младших битов номера символа, поставив их в младшие биты последнего октета, продолжить справа налево до первого октета. Свободные биты первого октета, оставшиеся незадействованными, заполнить нулями.
Примеры кодирования
[править | править код]| Символ | Двоичный код символа | UTF-8 в двоичном виде | UTF-8 в шестнадцатеричном виде | |
|---|---|---|---|---|
| $ | U+0024
|
0100100
|
00100100
|
24
|
| ¢ | U+00A2
|
10100010
|
11000010 10100010
|
C2 A2
|
| € | U+20AC
|
100000 10101100
|
11100010 10000010 10101100
|
E2 82 AC
|
| 𐍈 | U+10348
|
1 00000011 01001000
|
11110000 10010000 10001101 10001000
|
F0 90 8D 88
|
Пример кодирования Unicode-строки в UTF-8 без предварительного просчёта длины на языке C
[править | править код]#ifdef _WIN32
typedef unsigned __int8 u_int8_t;
typedef unsigned __int16 u_int16_t;
typedef unsigned __int32 u_int32_t;
typedef unsigned __int64 u_int64_t;
typedef wchar_t uchar_t;
#else
#include <sys/types.h>
typedef u_int16_t uchar_t;
#endif
size_t UnicodeToUTF8(char** pDest, const uchar_t* Src, size_t SrcLen) {
size_t dlen = 0; // Длина целевой строки
char* offset = *pDest = NULL // Итератор по целевой строке
, header = (char)0x80 // "заголовок" первого байта
, fcap = 0b00111111; // Вместимость первого бийта
const uchar_t* src = Src // указатель на кодируемую точку
, * end = Src + SrcLen; // Указатель на конец исходной строки
uchar_t code = 0; // Кодируемая точка
u_int32_t* ibuf = NULL; // Указатель на целевую строку представленную как беззнаковое целое 32 бита
// Выделяем память, сохраняем указатель в *pDest, заодно инициализируем итератор
offset = *pDest = (char*)malloc(SrcLen * 4 + 4);
// Проверяем выделена ли память
if (NULL != offset) {
// основной цикл
while (src < end) {
/* Проверяем размерность исходного символа, если величина
меньше чем 0x80U, значит его кодировать не нужно */
if (*src < 0x80UL) {
// копируем значение
*(offset) = *(char*)(src);
}
else {
// Коируем кодовую точку
code = *src;
// сохраняем указатель на первый байт как на беззнаковое целое 32 бита
ibuf = (u_int32_t*)offset;
// Обнуляем 4 байта
(*ibuf) = 0UL;
// Инициализируем заголовок
header = (char)0x80;
// выставляем вместимость первого байта
fcap = 0b00111111U;
/* цикл непосредственно кодирования точки в последовательность байтов */
while (code) {
//
// сравниваем вместимость первого байта и остаток незакодированных битов
if (fcap < code) {
// вместимость не меньше остатка
// копируем младшие 6 битов
*ibuf |= code & 0b00111111UL;
// копируем маркер последующего байта
*ibuf |= 0x80UL;
// сдвигаем все биты в более старший байт
*ibuf <<= 8;
// убираем закодированные 6 битов
code >>= 6;
// сдвигаем "заголовок" первого байта на 1 вправо
header >>= 1;
// уменьшаем вместимость первого байта
fcap >>= 1;
// инкрементируем длину целевой строки
++dlen;
// смещаем указатель на на итоговую строку вправо на 1 символ
++offset;
}
else {
// сохраняем заголовок и оставшиеся биты в первый байт последовательности
*(u_int8_t*)ibuf |= (header | (u_int8_t)code);
// обнуляем кодовую точку
code = 0U;
}
}
}
// Смещаем указатель на целевую строку
++offset;
// Смещаем указатель на исходную строку
++src;
// Увеличиваем показатель длины итоговой целевой строки
++dlen;
}
}
// Проверяем на ошибки, если их нет - обозначаем конец целевой строки
if (dlen)
*offset = '\0';
// возвращаем итоговую длину
return dlen;
}
Алгоритм декодирования (десериализации)
[править | править код]Алгоритм декодирования требуется для того, чтобы преобразовать сырую последовательность октетов, в готовую для использования Юникод точку. Алгоритм состоит из 3-х этапов:
1. Определить длину символа в октетах. Для определения длины символа, декодер UTF-8 смотрит на служебные биты (то есть те, которые сами по себе не несут полезной нагрузки, но при этом критически важны для определения длины символа в октетах.
служебные биты в UTF-8 всегда старшие) установленные при кодировании Юникод точки в UTF-8 символ.
если служебные биты равны 110,1110 или 11110 то текущий октет — начало символа. если служебный бит равен 0 — это ASCII символ.если служебные биты равны 10 — текущий октет является продолжением.
2. Убрать служебные биты. Поскольку декодер UTF-8 уже определил длину символа, служебные биты больше не являются нужными, а также будут мешать этапу 3, именно поэтому декодер UTF-8 «вырезает» служебные биты из потока октетов используя побитовую операцию «И» с необходимой маской, обнуляющую служебные биты оставляя при этом биты с данными не подвергнутыми изменениям.
3. соединить изолированные биты в Юникод точку. После того, как декодер UTF-8 убрал служебные биты, его основной задачей является соединить изолированные между собой биты в Юникод точку.
Для этого декодер UTF-8 использует побитовый оператор «сдвига влево» (<<) который выполняет 2 операции:
1. перемещает младшие в кодировке UTF-8 биты на их изначальную, более старшую позицию.
2. защищает от наложений групп битов друг на друга тем, что перемещает группу битов в другое положение, оставляя свободное место для других групп битов.
После того, как декодер UTF-8 переместил младшие биты в кодировке UTF-8 в их изначальное более старшое место, он должен поместить и младшие биты исходного символа. Для этого он использует побитовую операцию «ИЛИ» (|), которая соединяет более младшую часть исходного символа, в уже существующий октет, где есть старшая часть исходного символа.
Пример декодирования Unicode-строки из UTF-8 без предварительного расчёта итоговой длины на языке C
[править | править код]#ifdef _WIN32
typedef unsigned __int8 u_int8_t;
typedef unsigned __int16 u_int16_t;
typedef unsigned __int32 u_int32_t;
typedef unsigned __int64 u_int64_t;
typedef wchar_t uchar_t;
#else
#include <sys/types.h>
typedef u_int16_t uchar_t;
#endif
size_t UTF8ToUnicode16LE(uchar_t** Dest, char* Src, size_t SrcLen) {
// Буффер для ыорматированного вывода ошибок
char buf[256];
u_int8_t
*src = (u_int8_t*)Src, // Указатель на обрабатываемый байт
*end = (u_int8_t*)(Src + SrcLen); // Указатель на конец исходной строки
uchar_t
*dest = // Указатель на декодируемую точку целевой строки
(*Dest) = (uchar_t*)malloc((end - src + 1) * sizeof(**Dest)), // Целевая строка
head; // сюда сохраняем первый байт последовательности
u_int8_t
tdata = 0b00111111U, // Маска значимых битов последующих байтов
check = 0b01000000U, // Маска проверки последующих байтов
fdata, // Маска значащих битов первого байта
iter, // Маска-итератор первого байта
fshift; // счетчик сдвига битов первого байта
/* Проверяем выделилась ли память */
if (dest) {
/* Инициализируем выделенную память нулями */
memset(dest, 0, sizeof(**Dest) * SrcLen + sizeof(**Dest));
/* Основной цикл прохода по исходной строке */
for (; src < end; src++, dest++) {
/* Если значение первого байта меньше 0x80 (128) */
if ((*src) < 0x80UL) {
// Значение меньше 128
(*dest) = (*src);
}
else if (((*src) & 0b11000000U) == 0b11000000U) {
/* Значение больше либо равно 192 */
// сохраняем первый байт(заголовок)
head = *src;
// Сбрасываем счётчик сдвига
fshift = 0;
// Сбрасываем маску-итератор
iter = 0b01000000U;
// Сбрасываем маску значащих битов первого байта
fdata = 0b00111111U;
while (iter & head) {
// Сдвигаем указатель на последовательность на следующий байт
++src;
/* Проверяем битовый маркер */
if (((*src) ^ 0b01000000U) >= 0b11000000U) {
// Сдвигаем биты влево на 6(количество значащих битов)
(*dest) <<= 6;
// Копируем младшие биты
(*dest) |= ((*src) & 0b00111111U);
// Увеличиваем счетчик сдвига
fshift += 6;
// Сдвигаем маску значащих битов первого байта вправо на 1 бит
fdata >>= 1;
// Сдвигаем маску-итератор вправо на 1 бит
iter >>= 1;
}
else {
/* Обрабатываем ошибку кодирования.
Маркером ошибки служит обнуленный указатель на декодируемую точку */
sprintf(buf, "Ошибка кодирования. Ожидалось: '0b10xxxxxx'. Индекс: %llu\n", dest - *Dest);
perror(buf);
// обнуляем указатель на декодируемую точку
dest = NULL;
break;
}
}
/* Проверяем указатель на кодируемую точку */
if (dest)
// Копируем значащие биты первого байта сдвинув влево подсчитаное количество раз
*dest |= ((uchar_t)(head & fdata) << fshift);
else {
// если Указатель обнулен - выходим из основного цикла
break;
}
}
else {
// Обрабатываем ошибку кодирования первого байта
sprintf(buf, "Ошибка кодирования. Ожидалось: '0b11xxxxxx' получено '0b10xxxxxx' Индекс: %llu\n", dest - *Dest);
perror(buf);
dest = NULL;
break;
}
}
// Проверяем на ошибки
if (NULL != dest) {
// На всякий случай обнуляем последний символ целевой строки
*dest = (uchar_t)0U;
}
else {
// Высвобождаем память
free(*Dest);
// Обнуляем целевой указатель для безопасности
*Dest = NULL;
}
}
// Ели возникла ошибка то вернётся 0, т.к. оба указателя будут равны NULL
return dest - *Dest;
}
Маркер UTF-8
[править | править код]Для указания, что файл или поток содержит символы Юникода, в начале файла или потока может быть вставлен маркер последовательности байтов (англ. Byte order mark, BOM), который в случае кодирования в UTF-8 принимает форму трёх октетов: EF BB BF16.
| 1-й байт | 2-й байт | 3-й байт | |
|---|---|---|---|
| Двоичный код | 1110 1111 |
1011 1011 |
1011 1111
|
| Шестнадцатеричный код | EF
|
BB
|
BF
|
Пятый и шестой октеты
[править | править код]Изначально кодировка UTF-8 допускала использование до шести октетов для кодирования одного символа, однако в ноябре 2003 года стандарт RFC 3629 запретил использование пятого и шестого октетов, а диапазон кодируемых символов был ограничен символом U+10FFFF. Это было сделано для обеспечения совместимости с UTF-16.
Возможные ошибки декодирования
[править | править код]Не всякая последовательность октетов является допустимой. Декодер UTF-8 должен понимать и адекватно обрабатывать такие ошибки:
- Недопустимый октет.
- октет продолжения (10xxxxxx) без начального октета.
- Отсутствие нужного количества октетов продолжения 10xxxxxx — например, двух после 1110xxxx.
- Строка обрывается посреди символа.
- Неэкономное кодирование — например, кодирование символа тремя октетами, когда можно двумя (существует нестандартный вариант UTF-8, который кодирует символ с кодом 0 как 1100.0000 1000.0000, отличая его от символа конца строки 0000.0000.)
- Последовательность октетов, декодирующаяся в недопустимую кодовую позицию (например символы суррогатных пар UTF-16).
Примечания
[править | править код]- ↑ Usage Statistics of Character Encodings for Websites, June 2011 (англ.)
- ↑ Архивированная копия (англ.). Дата обращения: 27 февраля 2007. Архивировано из оригинала 1 марта 2011 года.
- ↑ Code Page Identifiers — Windows applications | Microsoft Docs. Дата обращения: 14 июля 2018. Архивировано 16 июня 2019 года.
- ↑ Well, I'm Back. String Theory (англ.). Robert O'Callahan (1 марта 2008). Дата обращения: 1 марта 2008. Архивировано 23 августа 2011 года.
- ↑ Ростислав Чебыкин. Всем кодировкам кодировка. UTF‑8: современно, грамотно, удобно. HTML и CSS. Дата обращения: 22 марта 2009. Архивировано 23 августа 2011 года.
Ссылки
[править | править код]- UTF-8 Стандарт кодирования
- UTF-8 encoding table and Unicode characters
- UTF-8: Кодирование и декодирование
- UTF-8, UTF-16, UTF-32 & BOM — Вопросы и ответы
- Compatibility Encoding Scheme for UTF-16: 8-Bit (CESU-8)
- Полное описание стандарта Unicode
- UTF-8 Everywhere Manifesto
- RFC-3629 «UTF-8, a transformation format of ISO 10646»