Как файл с китайскими иероглифами знает, сколько байтов нужно использовать на символ? - PullRequest
20 голосов
/ 22 апреля 2009

Я прочитал статью Джоэла "Абсолютный минимум, который должен знать каждый разработчик программного обеспечения. Абсолютно, положительно должен знать о юникоде и наборах символов (никаких оправданий!)" , но все еще не понимаю всех деталей. Пример проиллюстрирует мои проблемы. Посмотрите на этот файл ниже:

alt text
(источник: yart.com.au )

Я открыл файл в бинарном редакторе, чтобы внимательно изучить последние три из трех рядом с первым китайским символом:

alt text
(источник: yart.com.au )

По Джоэлу:

В UTF-8 каждая кодовая точка от 0 до 127 сохраняется в одном байте. Только кодовые точки 128 и выше сохраняются с использованием 2, 3, фактически до 6 байтов.

Так говорит редактор:

  1. E6 (230) выше кодовой точки 128.
  2. Таким образом, я буду интерпретировать следующие байты как 2, 3, фактически до 6 байтов.

Если это так, что означает, что интерпретация превышает 2 байта? Как это указывается байтами, которые следуют за E6?

Мой китайский символ хранится в 2, 3, 4, 5 или 6 байтах?

Ответы [ 9 ]

28 голосов
/ 22 апреля 2009

Если кодировка UTF-8, то в следующей таблице показано, как кодовая точка Unicode (до 21 бита) преобразуется в кодировку UTF-8:

Scalar Value                 1st Byte  2nd Byte  3rd Byte  4th Byte
00000000 0xxxxxxx            0xxxxxxx
00000yyy yyxxxxxx            110yyyyy  10xxxxxx
zzzzyyyy yyxxxxxx            1110zzzz  10yyyyyy  10xxxxxx
000uuuuu zzzzyyyy  yyxxxxxx  11110uuu  10uuzzzz  10yyyyyy  10xxxxxx

Существует ряд недопустимых значений - в частности, байты 0xC1, 0xC2 и 0xF5 - 0xFF никогда не могут появляться в правильно сформированном UTF-8. Есть также ряд других комбинаций verboten. Неровности в столбцах 1-го и 2-го байта. Обратите внимание, что коды U + D800 - U + DFFF зарезервированы для суррогатов UTF-16 и не могут отображаться в действительном UTF-8.

Code Points          1st Byte  2nd Byte  3rd Byte  4th Byte
U+0000..U+007F       00..7F
U+0080..U+07FF       C2..DF    80..BF
U+0800..U+0FFF       E0        A0..BF    80..BF
U+1000..U+CFFF       E1..EC    80..BF    80..BF
U+D000..U+D7FF       ED        80..9F    80..BF
U+E000..U+FFFF       EE..EF    80..BF    80..BF
U+10000..U+3FFFF     F0        90..BF    80..BF    80..BF
U+40000..U+FFFFF     F1..F3    80..BF    80..BF    80..BF
U+100000..U+10FFFF   F4        80..8F    80..BF    80..BF

Эти таблицы взяты из Unicode стандартной версии 5.1.


В вопросе материал со смещением 0x0010 .. 0x008F дает:

0x61           = U+0061
0x61           = U+0061
0x61           = U+0061
0xE6 0xBE 0xB3 = U+6FB3
0xE5 0xA4 0xA7 = U+5927
0xE5 0x88 0xA9 = U+5229
0xE4 0xBA 0x9A = U+4E9A
0xE4 0xB8 0xAD = U+4E2D
0xE6 0x96 0x87 = U+6587
0xE8 0xAE 0xBA = U+8BBA
0xE5 0x9D 0x9B = U+575B
0x2C           = U+002C
0xE6 0xBE 0xB3 = U+6FB3
0xE6 0xB4 0xB2 = U+6D32
0xE8 0xAE 0xBA = U+8BBA
0xE5 0x9D 0x9B = U+575B
0x2C           = U+002C
0xE6 0xBE 0xB3 = U+6FB3
0xE6 0xB4 0xB2 = U+6D32
0xE6 0x96 0xB0 = U+65B0
0xE9 0x97 0xBB = U+95FB
0x2C           = U+002C
0xE6 0xBE 0xB3 = U+6FB3
0xE6 0xB4 0xB2 = U+6D32
0xE4 0xB8 0xAD = U+4E2D
0xE6 0x96 0x87 = U+6587
0xE7 0xBD 0x91 = U+7F51
0xE7 0xAB 0x99 = U+7AD9
0x2C           = U+002C
0xE6 0xBE 0xB3 = U+6FB3
0xE5 0xA4 0xA7 = U+5927
0xE5 0x88 0xA9 = U+5229
0xE4 0xBA 0x9A = U+4E9A
0xE6 0x9C 0x80 = U+6700
0xE5 0xA4 0xA7 = U+5927
0xE7 0x9A 0x84 = U+7684
0xE5 0x8D 0x8E = U+534E
0x2D           = U+002D
0x29           = U+0029
0xE5 0xA5 0xA5 = U+5965
0xE5 0xB0 0xBA = U+5C3A
0xE7 0xBD 0x91 = U+7F51
0x26           = U+0026
0x6C           = U+006C
0x74           = U+0074
0x3B           = U+003B
22 голосов
/ 22 апреля 2009

Это все часть кодировки UTF8 (которая является единственной схемой кодирования для Unicode).

Размер можно выяснить, изучив первый байт следующим образом:

  • если он начинается с битовой комбинации "10" (0x80-0xbf), это не первый байт последовательности, и вы должны выполнять резервное копирование до тех пор, пока не найдете начало, любой байт, начинающийся с «0» или «11» (спасибо Джеффри Хантину за указывая на это в комментариях).
  • если он начинается с битовой комбинации "0" (0x00-0x7f), это 1 байт.
  • если он начинается с битовой комбинации "110" (0xc0-0xdf), это 2 байта.
  • если он начинается с битовой комбинации "1110" (0xe0-0xef), это 3 байта.
  • если он начинается с битовой комбинации "11110" (0xf0-0xf7), это 4 байта.

Я продублирую таблицу, показывающую это, но оригинал находится на странице Википедии UTF8 здесь .

+----------------+----------+----------+----------+----------+
| Unicode        | Byte 1   | Byte 2   | Byte 3   | Byte 4   |
+----------------+----------+----------+----------+----------+
| U+0000-007F    | 0xxxxxxx |          |          |          |
| U+0080-07FF    | 110yyyxx | 10xxxxxx |          |          |
| U+0800-FFFF    | 1110yyyy | 10yyyyxx | 10xxxxxx |          |
| U+10000-10FFFF | 11110zzz | 10zzyyyy | 10yyyyxx | 10xxxxxx |
+----------------+----------+----------+----------+----------+

Символы Юникода в приведенной выше таблице составлены из битов:

000z-zzzz yyyy-yyyy xxxx-xxxx

где биты z и y предполагаются равными нулю, если они не заданы. Некоторые байты считаются недопустимыми в качестве начального байта, поскольку они либо:

  • бесполезно: 2-байтовая последовательность, начинающаяся с 0xc0 или 0xc1, фактически дает кодовую точку, меньшую 0x80, которую можно лучше представить с помощью 1-байтовой последовательности.
  • используется RFC3629 для 4-байтовой последовательности выше U + 10FFFF или 5-байтовой и 6-байтовой последовательностей. Это байты от 0xf5 до 0xfd.
  • просто не используется: байты 0xfe и 0xff.

Кроме того, последующие байты в многобайтовой последовательности, которые не начинаются с битов "10", также недопустимы.

В качестве примера рассмотрим последовательность [0xf4,0x8a, 0xaf, 0x8d]. Это 4-байтовая последовательность, поскольку первый байт находится в диапазоне от 0xf0 до 0xf7.

    0xf4     0x8a     0xaf     0x8d
= 11110100 10001010 10101111 10001101
       zzz   zzyyyy   yyyyxx   xxxxxx

= 1 0000 1010 1011 1100 1101
  z zzzz yyyy yyyy xxxx xxxx

= U+10ABCD

Для вашего конкретного запроса с первым байтом 0xe6 (длина = 3) последовательность байтов:

    0xe6     0xbe     0xb3
= 11100110 10111110 10110011
      yyyy   yyyyxx   xxxxxx

= 01101111 10110011
  yyyyyyyy xxxxxxxx

= U+6FB3

Если вы посмотрите этот код здесь , вы увидите, что он был в вашем вопросе: 澳.

Чтобы показать, как работает декодирование, я вернулся к своим архивам, чтобы найти код обработки UTF8. Мне пришлось немного изменить его, чтобы сделать его полноценной программой, и кодировка была удалена (поскольку вопрос был действительно о декодировании), поэтому я надеюсь, что я не внес никаких ошибок из вырезки и вставки:

#include <stdio.h>
#include <string.h>

#define UTF8ERR_TOOSHORT -1
#define UTF8ERR_BADSTART -2
#define UTF8ERR_BADSUBSQ -3
typedef unsigned char uchar;

static int getUtf8 (uchar *pBytes, int *pLen) {
    if (*pLen < 1) return UTF8ERR_TOOSHORT;

    /* 1-byte sequence */
    if (pBytes[0] <= 0x7f) {
        *pLen = 1;
        return pBytes[0];
    }

    /* Subsequent byte marker */
    if (pBytes[0] <= 0xbf) return UTF8ERR_BADSTART;

    /* 2-byte sequence */
    if ((pBytes[0] == 0xc0) || (pBytes[0] == 0xc1)) return UTF8ERR_BADSTART;
    if (pBytes[0] <= 0xdf) {
        if (*pLen < 2) return UTF8ERR_TOOSHORT;
        if ((pBytes[1] & 0xc0) != 0x80) return UTF8ERR_BADSUBSQ;
        *pLen = 2;
        return ((int)(pBytes[0] & 0x1f) << 6)
            | (pBytes[1] & 0x3f);
    }

    /* 3-byte sequence */
    if (pBytes[0] <= 0xef) {
        if (*pLen < 3) return UTF8ERR_TOOSHORT;
        if ((pBytes[1] & 0xc0) != 0x80) return UTF8ERR_BADSUBSQ;
        if ((pBytes[2] & 0xc0) != 0x80) return UTF8ERR_BADSUBSQ;
        *pLen = 3;
        return ((int)(pBytes[0] & 0x0f) << 12)
            | ((int)(pBytes[1] & 0x3f) << 6)
            | (pBytes[2] & 0x3f);
    }

    /* 4-byte sequence */
    if (pBytes[0] <= 0xf4) {
        if (*pLen < 4) return UTF8ERR_TOOSHORT;
        if ((pBytes[1] & 0xc0) != 0x80) return UTF8ERR_BADSUBSQ;
        if ((pBytes[2] & 0xc0) != 0x80) return UTF8ERR_BADSUBSQ;
        if ((pBytes[3] & 0xc0) != 0x80) return UTF8ERR_BADSUBSQ;
        *pLen = 4;
        return ((int)(pBytes[0] & 0x0f) << 18)
            | ((int)(pBytes[1] & 0x3f) << 12)
            | ((int)(pBytes[2] & 0x3f) << 6)
            | (pBytes[3] & 0x3f);
    }

    return UTF8ERR_BADSTART;
}

static uchar htoc (char *h) {
    uchar u = 0;
    while (*h != '\0') {
        if ((*h >= '0') && (*h <= '9'))
            u = ((u & 0x0f) << 4) + *h - '0';
        else
            if ((*h >= 'a') && (*h <= 'f'))
                u = ((u & 0x0f) << 4) + *h + 10 - 'a';
            else
                return 0;
        h++;
    }
    return u;
}

int main (int argCount, char *argVar[]) {
    int i;
    uchar utf8[4];
    int len = argCount - 1;

    if (len != 4) {
            printf ("Usage: utf8 <hex1> <hex2> <hex3> <hex4>\n");
            return 1;
    }
    printf ("Input:      (%d) %s %s %s %s\n",
        len, argVar[1], argVar[2], argVar[3], argVar[4]);

    for (i = 0; i < 4; i++)
            utf8[i] = htoc (argVar[i+1]);

    printf ("   Becomes: (%d) %02x %02x %02x %02x\n",
        len, utf8[0], utf8[1], utf8[2], utf8[3]);

    if ((i = getUtf8 (&(utf8[0]), &len)) < 0)
        printf ("Error %d\n", i);
    else
        printf ("   Finally: U+%x, with length of %d\n", i, len);

    return 0;
}

Вы можете запустить его с вашей последовательностью байтов (вам понадобится 4, поэтому используйте 0 для их заполнения) следующим образом:

> utf8 f4 8a af 8d
Input:      (4) f4 8a af 8d
   Becomes: (4) f4 8a af 8d
   Finally: U+10abcd, with length of 4

> utf8 e6 be b3 0
Input:      (4) e6 be b3 0
   Becomes: (4) e6 be b3 00
   Finally: U+6fb3, with length of 3

> utf8 41 0 0 0
Input:      (4) 41 0 0 0
   Becomes: (4) 41 00 00 00
   Finally: U+41, with length of 1

> utf8 87 0 0 0
Input:      (4) 87 0 0 0
   Becomes: (4) 87 00 00 00
Error -2

> utf8 f4 8a af ff
Input:      (4) f4 8a af ff
   Becomes: (4) f4 8a af ff
Error -3

> utf8 c4 80 0 0
Input:      (4) c4 80 0 0
   Becomes: (4) c4 80 00 00
   Finally: U+100, with length of 2
5 голосов
/ 22 апреля 2009

Отличным справочным материалом для этого являются UTC-8 и Unicode от Markus Kuhn .

3 голосов
/ 22 апреля 2009

По сути, если он начинается с 0, это 7-битная кодовая точка. Если он начинается с 10, это продолжение многобайтовой кодовой точки. В противном случае число 1 говорит вам, сколько байтов кодируется этой кодовой точкой.

Первый байт указывает, сколько байтов кодирует кодовую точку.

0xxxxxxx 7 битов кодовой точки, закодированных в 1 байт

110xxxxx 10xxxxxx 10 битов кодовой точки, закодированных в 2 байта

110xxxxx 10xxxxxx 10xxxxxx и т. Д. 1110xxxx 11110xxx и т.д.

2 голосов
/ 22 апреля 2009

UTF-8 построен таким образом, что нет никакой двусмысленности относительно того, где начинается символ и сколько у него байтов.

Это действительно просто.

  • Байт в диапазоне от 0x80 до 0xBF равен никогда первому байту символа.
  • Любой другой байт всегда первый байт символа.

UTF-8 имеет много избыточности.

Если вы хотите определить длину байта символа, есть несколько способов.

  • Первый байт всегда говорит вам, сколько байтов имеет длина символ:
    • Если первый байт от 0x00 до 0x7F, это один байт.
    • 0xC2 до 0xDF означает, что это два байта.
    • 0xE0 до 0xEF означает, что это три байта.
    • 0xF0 до 0xF4 означает, что это четыре байта.
  • Или вы можете просто посчитать количество последовательных байтов в диапазоне от 0x80 до 0xBF, поскольку все эти байты принадлежат к тому же символу, что и предыдущий байт.

Некоторые байты никогда не используются, например, от 0xC1 до 0xC2 или от 0xF5 до 0xFF, поэтому, если вы встретите эти байты где-либо, то вы не смотрите на UTF-8.

2 голосов
/ 22 апреля 2009
2 голосов
/ 22 апреля 2009

Кодовые точки до 0x7ff сохраняются как 2 байта; до 0xffff как 3 байта; все остальное как 4 байта. (Технически, до 0x1fffff, но максимальный допустимый код в Unicode - 0x10ffff.)

При декодировании первый байт многобайтовой последовательности используется для определения количества байтов, использованных для создания последовательности:

  1. 110x xxxx => 2-байтовая последовательность
  2. 1110 xxxx => 3-байтовая последовательность
  3. 1111 0xxx => 4-байтовая последовательность

Все последующие байты в последовательности должны соответствовать шаблону 10xx xxxx.

1 голос
/ 22 апреля 2009

Подсказка в этом предложении здесь:

В UTF-8 каждая кодовая точка от 0 до 127 хранится в одном байте. Только код точки 128 и выше сохраняются с использованием 2, 3, по сути, до 6 байтов.

У каждой кодовой точки до 127 верхний бит установлен в ноль. Следовательно, редактор знает, что если он встречает байт, где верхний бит равен 1, это начало многобайтового символа.

0 голосов
/ 04 января 2013

почему так много сложных ответов?

3 байта для 1 китайского символа. используя эту функцию (под jQuery):

function get_length(field_selector) {
  var escapedStr = encodeURI($(field_selector).val())
  if (escapedStr.indexOf("%") != -1) {
    var count = escapedStr.split("%").length - 1
    if (count == 0) count++  //perverse case; can't happen with real UTF-8
    var tmp = escapedStr.length - (count * 3)
    count = count + tmp
  } else {
    count = escapedStr.length
  }
  return count
}
...