21

我已阅读 Joel 的文章“每个软件开发人员绝对、肯定必须了解 Unicode 和字符集(没有借口!)”,但仍然不了解所有细节。一个例子将说明我的问题。看看下面这个文件:

替代文字
(来源:yart.com.au

我在二进制编辑器中打开了文件,仔细检查了第一个汉字旁边的三个 a 中的最后一个:

替代文字
(来源: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 字节存储的?

4

9 回答 9

28

如果编码为 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 中。还有许多其他的禁止组合。不规则出现在第 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
于 2009-04-22T01:49:19.003 回答
22

这就是 UTF8 编码的全部内容(这只是 Unicode 的一种编码方案)。

大小可以通过检查第一个字节来计算,如下所示:

  • 如果它以 bit pattern 开头"10" (0x80-0xbf),则它不是序列的第一个字节,您应该备份直到找到开头,任何以“0”或“11”开头的字节(感谢 Jeffrey Hantin 在评论中指出这一点)。
  • 如果它以位模式开头,则为"0" (0x00-0x7f)1 个字节。
  • 如果它以位模式开头,则为"110" (0xc0-0xdf)2 个字节。
  • 如果它以位模式开头,则为"1110" (0xe0-0xef)3 个字节。
  • 如果它以位模式开头,则为"11110" (0xf0-0xf7)4 个字节。

我将复制显示此内容的表格,但原件在此处的 Wikipedia 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 |
+----------------+----------+----------+----------+----------+

上表中的 Unicode 字符由以下位构成:

000z-zzzz yyyy-yyyy xxxx-xxxx

在没有给出的情况下,zy位被假定为零。有些字节被认为是非法的起始字节,因为它们是:

  • 无用:以 0xc0 或 0xc1 开头的 2 字节序列实际上给出了小于 0x80 的代码点,可以用 1 字节序列更好地表示。
  • RFC3629 用于 U+10FFFF 以上的 4 字节序列,或 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
于 2009-04-22T01:46:56.247 回答
5

一个很好的参考是 Markus Kuhn 的UTF-8 and Unicode FAQ

于 2009-04-22T01:50:41.807 回答
3

本质上,如果它以 0 开头,则它是一个 7 位代码点。如果它以 10 开头,则它是多字节代码点的延续。否则,1 的数量告诉您此代码点编码为多少字节。

第一个字节表示编码代码点的字节数。

0xxxxxxx 7 位代码点编码为 1 个字节

110xxxxxx 10xxxxxx 10 位代码点,编码为 2 个字节

110xxxxx 10xxxxxx 10xxxxxx 等 1110xxxx 11110xxx 等

于 2009-04-22T01:48:00.003 回答
2

最多 0x7ff 的代码点存储为 2 个字节;最多 0xffff 为 3 个字节;其他所有内容为 4 个字节。(从技术上讲,最高为 0x1fffff,但 Unicode 中允许的最高代码点为 0x10ffff。)

解码时,多字节序列的第一个字节用于确定用于制作序列的字节数:

  1. 110x xxxx=> 2字节序列
  2. 1110 xxxx=> 3字节序列
  3. 1111 0xxx=> 4字节序列

序列中的所有后续字节都必须符合该10xx xxxx模式。

于 2009-04-22T01:45:48.480 回答
2

3 个字节
http://en.wikipedia.org/wiki/UTF-8#Description

于 2009-04-22T01:55:44.477 回答
2

UTF-8 的构造方式使得字符从哪里开始以及它有多少字节不会有歧义。

这真的很简单。

  • 0x80 到 0xBF 范围内的字节绝不是字符的第一个字节。
  • 任何其他字节始终是字符的第一个字节。

UTF-8 有很多冗余。

如果你想知道一个字符有多少字节,有多种方法可以知道。

  • 第一个字节总是告诉你字符有多少字节:
    • 如果第一个字节是 0x00 到 0x7F,则为一个字节。
    • 0xC2 到 0xDF 表示它是两个字节。
    • 0xE0 到 0xEF 表示它是三个字节。
    • 0xF0 到 0xF4 表示它是四个字节。
  • 或者,您可以只计算 0x80 到 0xBF 范围内的连续字节数,因为这些字节都与前一个字节属于同一个字符。

有些字节从不使用,例如 0xC1 到 0xC2 或 0xF5 到 0xFF,所以如果你在任何地方遇到这些字节,那么你就不是在看 UTF-8。

于 2009-04-22T05:08:48.430 回答
1

提示在这句话中:

在 UTF-8 中,从 0 到 127 的每个代码点都存储在一个字节中。只有 128 及以上的代码点使用 2、3 存储,实际上最多 6 个字节。

最多 127 的每个代码点都将最高位设置为零。因此,编辑器知道,如果遇到最高位为 1 的字节,就是多字节字符的开始。

于 2009-04-22T01:45:03.880 回答
0

为什么有这么多复杂的答案?

1 个汉字 3 个字节。使用这个函数(在 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
}
于 2013-01-04T07:53:45.207 回答