喵の窝

UTF8编码的处理 (上)

utf8所带来的问题

目前来看,utf8带来的问题主要有两个

  • 每个字符编码的长度不定
  • 同一个字符有多种表示方法

每个字符编码长度不定

utf8采用1-4个字节编码。这也就是说同样储存一个字符,有些字符(比如英文字母)只需要一个字节。而有些字符(比如中文汉字)则需要三个字节 [注1] 。于是这就非常尴尬了。比如说字符串”abc一二三🌚”用utf8编码来存储的话,需要占用16个字节来存储。”abc”三个英文字符每个占用一个字节。”一二三”三个中文字符每个占用3字节,而emoji符号🌚需要用四个字节来存储。每个字节存储的内容如下

1
2
 'a'   | 'b'  | 'c'  | '一'           | '二'           | '三'           | '🌚'
[ 0x61 | 0x62 | 0x63 | 0xe4 0xb8 0x80 | 0xe4 0xba 0x8c | 0xe4 0xb8 0x89 | 0xf0 0x9f 0x8c 0x9a]

如果我们用一个byte数组来存这些字符串的话,那就会遇到一些非常尴尬的问题。比如下面的代码能正确处理英文字符,但是如果遇到英文以外的字符就不能工作了。

1
2
3
4
5
6
7
// 遍历字符串中每一个字符
uint8_t str[16] = {0x61, 0x62, 0x63, 0xe4, 0xb8, 0x80, 0xe4, 0xba,
0x8c, 0xe4, 0xb8, 0x89, 0xf0, 0x9f, 0x8c, 0x9a};
for (unsigned long i = 0; i < sizeof(str) / sizeof(str[0]); ++i)
{
printf("%c ", str[i]);
}

类似的问题还在出现在strlen函数中。
通常来说,解决这个问题的方法就是把’字符’和’字节’分开对待。上面的代码之所以能处理英文字符是因为utf8编码正好用一个字节来储存英文字符。而到了其他语言环境,由于储存一个字符需要多个字节,所以这种按字节遍历的方式就不工作了。
目前,无论是C语言还是C++语言都有utf8的解析库。而新出现的语言比如swift和GoLang则是在语言设计的阶段把utf8的问题解决了。
下面,我们以GoLang为例看看如何解决这个问题。
在GoLang中,表示字符串类型的string和表示字节数组的[]byte可以相互转化(当然,[]byte必须是一段utf8编码的字符串)。这样,如果我们从外界读取到一段byte数组时,我们可以直接将其转换成字符串,然后再对字符串处理。
其次,在GoLang中使用for-range对字符串遍历的话,是对字符串中每一个字符进行遍历。这也就是说,用for-range遍历字符串的话,GoLang会自动判断当前字符所占用的字节数,并且返回对应的字符,同时在下轮遍历时跳过对应的字节。请看下面一段代码。

1
2
3
4
str := "abc一二三🌚"
for index, ch := range str {
fmt.Printf("下标: %d\t字符: %c\n", index, ch)
}

这段代码用for-range遍历字符串,打印出每个字符和对应的角标。这段程序的输出如下:

1
2
3
4
5
6
7
下标: 0	字符: a
下标: 1 字符: b
下标: 2 字符: c
下标: 3 字符: 一
下标: 6 字符: 二
下标: 9 字符: 三
下标: 12 字符: 🌚

可以看到,由于英文用一个字节储存一个字符,所以英文的下标是连续的,而中文用三个字节储存一个字符,所以角标间隔了3。
除了for-range以外。GoLang的unicode/utf8包提供了对utf8编码字符串的强大支持。上面的代码用传统的for循环也能实现。代码如下

1
2
3
4
5
6
7
str := "abc一二三🌚"
strByte := []byte(str)
for i := 0; i < len(str); {
ch, size := utf8.DecodeRune(strByte[i:])
fmt.Printf("下标: %d\t字符: %c\n", i, ch)
i += size
}

我们先将字符串转化成byte数组,这个转化不涉及到内存拷贝,所以时间复杂度为O(1)。然后,调用utf8包中的DecodeRune方法解析byte数组。这个方法有两个返回值,第一个返回值为解析出的字符,第二个返回值为该字符所占用的字节数。代码的输出和上一段代码一样。

以上演示的方法都是以字符为单位来遍历。所以对于utf8的字符串或者byte数组都能正常处理。下面我们来看看如果按照字节遍历会是什么结果

1
2
3
4
5
str := "abc一二三🌚"
strByte := []byte(str)
for i := 0; i < len(str); i++ {
fmt.Printf("下标: %d\t字符: %c\n", i, strByte[i])
}

输出如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
下标: 0	字符: a
下标: 1 字符: b
下标: 2 字符: c
下标: 3 字符: ä
下标: 4 字符: ¸
下标: 5 字符: €
下标: 6 字符: ä
下标: 7 字符: º
下标: 8 字符: Œ
下标: 9 字符: ä
下标: 10 字符: ¸
下标: 11 字符: ‰
下标: 12 字符: ð
下标: 13 字符: Ÿ
下标: 14 字符: Œ
下标: 15 字符: š

可以看到,因为英文由于每个字符占用一个字节,所以能正常显示。而至于中文和最后的emoji表情嘛。。。呵呵。。。🌚


注1: 请务必区分’字节’和’字符’的区别。
一个字节指8个二进制位,是一段数据的大小。
一个字符指Unicode字符集能表示的一个符号。
一个中文汉字,一个英文字母,一个片假名,一个平假名,一个韩国汉字,一个俄文字母,一个emoji符号都算一个字符。


To be Continued …