032-unicode 与 utf-8

字符编码一直是让人头疼的问题。

unicode, utf-8, ascii 你知多少?有没有那么几次乱码问题让你夜不能寐?有没有因为编码问题你百度/谷哥过无数次也得不到你想要的答案?

好了,希望你在读完这一篇后,能对你有所帮助。

1. 字符集

Long long ago, 世界还很简单。

那个时候,计算机还没那么普及,128 种符号完全可以胜任。26 个英文字母,10 个阿拉伯数字,常用的标点符号,运算符号,足以让米国同学玩的嗨起。

米国同学将这 128 个字符定义成一个字符集,并取了个名字叫 ascii 码,并指派 128 个数字和它一一对应。因为这 128 个数字都没有超过 256 这个大小,因此,这些字符在计算机内部只需要 1 个字节(8bit)就可以表示。

苦干年后,计算机普及到全世界,但是只能表示 128 个字符,很多国家就感觉不爽,于是对这包含了 128 个字符的字符集进行了扩展,变成了 256 个字符,又取了个新名字,叫扩展 ascii 码。但是这只解决了部分国家的需求。

最不爽的要数咱们中国人了,汉字就远远不止 256 个了,咋整?后来某个组织想了个办法,既然全世界的文字这么多,干脆咱们再搞个超大的字符集吧,256 个实在太少了。经过一顿折腾后,他们搞出了一个能容纳全世界字符的字符集,还给它取了个名字叫 unicode 字符集。

这个字符集,将全世界的字符都定义到一个数字上。这个数字,官方术语叫 unicode code point,中文叫unicode 码点

又经过一番折腾后,一直到现在,unicode 不断在扩充,截至到现在(2018年3月),unicode 的稳定版本已经到 10.0 了!这是他们的网站:http://www.unicode.org/versions/Unicode10.0.0。看它们的介绍:

Unicode 10.0 adds 8,518 characters, for a total of 136,690 characters. These additions include 4 new scripts, for a total of 139 scripts, as well as 56 new emoji characters.

也就是说,如今的 unicode 字符集已经包含了 136690 个字符。大佬们觉得这个字符集需要足够的牛逼,还添加了 emoji,也就是表情包。。。

^_^ 这帮人太会玩了,不过不管他们怎么扩充,扩充的字符都不能使用已经被定义过的码点。新的字符,只要使用那些还没被使用的码点就行了,至于新字符使用哪个码点,就是这帮人的事情了。

2. 在计算机中表示 unicode 字符

既然 unicode 标准已经帮我们定义了每个数字和字符之间的对应关系,那就好办了,想表示世界上任何一个字符都没问题。

在有些计算机语言里,只使用了 65536 个 unicode 码点,因此它们使用了 2 个字节的的 short 类型就足够。毕竟 65536 个码点已经足够表示全世界的文字了。

但是在 Go 语言里,为了照顾到某些被定义在 65536 以上的字符,使用了一个 4 字节的整数类型来描述 unicode 字符,这个类型在 Go 里叫 —— rune(读作:如恩),这个单词的中文含义叫『符文』,听起来就很厉害的样子。

4 字节宽度的 rune 可以表示 4294967296 个码点,随你 unicode 怎么扩充也是够用的。下面这段程序演示了 Go 语言中打印 rune 类型字符。

import "fmt"

func main() {
    var a rune = 0x00004e16
    b := 0x00004e17
    c := 0x00004e18

    fmt.Printf("%c %c %c\n", a, b, c) // Output: 世 丗 丘
}

3. 如何保存 unicode 字符到文件?

假如说你从来没有听说过 utf-8,按照 Go 语言里的思路,保存 unicode 字符很简单:既然每个字符都用一个 4 字节整数来表示,那就把这个 4 字节直接保存到文件里啊。

如果这样的话,对于一个字符序列,比如『北京欢迎您』这 5 个字符来说,保存到本地需要 20 个字节。对于一个单词 hello 来说,保存到本地也需要 20 个字符。

这个方案非常棒,只是有点消耗磁盘空间,尽管现在磁盘很不值钱,但是对于网络数据传输而言,这种消耗还是有点伤不起。我们急需要一种能够使用尽量少的字节来描述 unicode 字符的方法。

比方说,对于小于 128 的 Unicode 字符来说,就只使用 1 个字节就能表示它了。对于 [ 128 , 65536 ) 之间的 unicode 字符,就使用 2 个字节来表示它,在 [65536, 16777216) 之间的字符,就使用 3 个字节来表示,在 [16777216, 4294967296) 之间字符,就使用 4 个字节来表示。总结如下:

unicode 码点 编码方法(16 进制)
[0, 128) [00, ff]
[128, 65536) [0100, ffff]
[65536, 16777216) [010000, ffffff]
[16777216, 4294967296) [01000000, ffffffff]

不幸的是我们的方法存在漏洞。比如 ffff 这两个字节,到底是表示 2 个在 [0, 128) 之间的字符,还是表示 [128, 65536) 里的 1 个字符?

3.1 UTF-8 编码

不要再想了,Go 语言的两位发明者,Ken Thompson 和 Rob Pike 两位大神发明了一种很牛逼且没有歧义的办法,并且给这个方法取了个名字—— UTF-8. 这种方法用的映射关系像下面这样:

unicode 字符 编码方法(16 进制)
[0, 128) 0xxxxxxx
[128, 2048) 110xxxxx 10xxxxxx
[2048,65536) 1110xxxx 10xxxxxx 10xxxxxx
[65536, 1114111) 11110xxxx 10xxxxxx 10xxxxxx 10xxxxxx

有人问,那大于 1114111 的 unicode 字符咋整?别忘记了,目前 Unicode 10.0 一共才包含 136690 字符,在 65536 到 1114111 中间还有大量的 Unicode 码点未被定义呢!

UTF-8 比较牛逼的地方在于它不会产生歧义。因为它的编码方案非常有规律,对于一个 UTF-8 序列来说,随便从哪个字节开始,都能准确的找到起始字节。方法就是从当前字节往前找,只要找到不是 10xxxxxx 这样的字节就行了。

3.2 unicode 与 utf-8 之间的关系

看完了前面的部分,相信你能体会出来 unicode 与 utf-8 之间微妙的关系了吧。这里再总结一下:

  • unicode 将字符定义到一个唯一数字的一种方案,这个数字被称为 unicode 码点。
  • unicode 码点在不同计算机语言里,可以使用 2 个字节或者 4 个字节的整数表示。在 go 语言里使用 4 个字节的 rune 类型表示。
  • utf-8 是将 unicode 码点转换成可变字节长度的序列的一种编码方法。你可以直接使用 4 字节的 unicode 码点存储数据,但是将其编码成 utf-8 字节序列会更省空间。

3.3 go 里相关的函数

go 语言的 unicode/utf8 这个包提供了两个函数,用于 unicode 码点和 utf-8 字节之间进行互转:

  • unicode 码点编码成 utf-8 字节序列
func EncodeRune(p []byte, r rune) int
  • utf-8 字节序列编码成 unicode 码点
func DecodeRune(p []byte) (r rune, size int)

此外,utf8 包还提供了其它更多的函数,同学们可以自行查阅文档。

4. 实例

  • 例1:将 unicode 字符编码成 utf8 字节序列
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    var r rune = '中'
    fmt.Printf("%c\n", r)  // output: 中
    buf := make([]byte, 4)
    n := utf8.EncodeRune(buf, r) // 返回编码后的字节序列长度
    fmt.Printf("%s\n", buf) // output: 中

    for i := 0; i < n; i++ {
        fmt.Printf("%d:%c\n", i, buf[i]) // 打印字节
    }
    fmt.Println()
}
  • 例2:将 utf8 字节序列解码成 unicode 字符
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    b := []byte("Hello, 世界")

    for len(b) > 0 {
        r, size := utf8.DecodeRune(b) // 将字节序列中第一个字符解码成 rune,并返回解码的字节长度
        fmt.Printf("%c %d\n", r, size)
        b = b[size:]
    }
}

5. 总结

  • 掌握 unicode 与 utf-8
  • 掌握 go 处理 unicode 与 utf-8 的方法

猜你喜欢

转载自blog.csdn.net/q1007729991/article/details/79588474