前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >字符编码实战

字符编码实战

原创
作者头像
王磊-字节跳动
发布2021-03-07 20:23:27
1.7K0
发布2021-03-07 20:23:27
举报
文章被收录于专栏:01ZOO01ZOO

数字

在计算机中,所有的信息最终只会表现为 0 和 1,所以计算机看到的数据是这样的。

代码语言:txt
复制
00010101010001001000110001100101010101

那么就带来一个问题,怎么用二进制来表示我们程序中需要使用的信息呢,比如 数字、字符、表情等等。

首先数字的问题比较好解决。把特定长度 01 串看成一个二进制数字就可以。比如 int8 就表示一个 8bit 长度的二进制,也就是1个字节表示了一个 int8 类型的数字,这个数字的能表示的范围是 -128, 127,一共 256 个数字,这点比较好理解,因为 8位数字最多只可能表示 256 种情况,从 00000000 -> 11111111。至于数字和二进制的对应关系,这点和补码这种设计有关,简单来说就是正数的补码:与原码相同,比如 7 的补码表示是 00000111, 而负数的补码则是所有位取反并加一,比如 -7 的补码是 11111001

对于更大范围的数字,比如 int32 能表示点范围为 -2147483648到2147483647,需要占用 4个字节的长度,而 int64 (在 64位机器上, int == int64), 能表示的范围有 -9223372036854775808 到9223372036854775807,它要占用 8 个字节的长度。由于占用了超过一个字节的长度,又带来另外一个问题:即字节顺序(端序)的问题,即这几个字节是怎么排列呢,是高位放在前面还是高位放在后面?有兴趣的可以参考这里

另外浮点数的表示方法则要稍微复杂一点,不过这不是本文的重点,有兴趣点可以看这里

AscII 码

解决了表示数字的问题,接下来就是字符的问题。为了解决这个问题,首先出现了 AscII 码. AscII 码虽然使用一个字节表示,但是实际只占用了其中的 7 个bit,表示了共计 128 个字符,第一个 bit 统一为 0。其中 32 个为控制字符【即不可打印,用作控制】,剩下的为可见字符。在 python 中比较为人熟知的函数 chr, ord 就是用来做 AscII 码和 对应字符的转换的,比如下面的例子

代码语言:txt
复制
>>> chr(65)
'A'
>>> chr(97)
'a'
>>> ord('A')
65

所有的字符表可以参考这里 和下面点表格(来自维基百科)

image
image
image
image

Unicode 编码

AscII 编码固然简单,但是只能解决英语世界的字母问题,用于表示中文等其他语言,显然是不够的。所以,中国制定了 GB2312 编码,用来把中文编进去。GB/T 2312标准共收录6763个汉字,其中一级汉字3755个,二级汉字3008个。每个汉字及符号以两个字节来表示。GBK 可以看成是 GB2312 的兼容扩展,共收入 21886 个汉字和图形符号。举个例子:"啊" 在 支持 GBK/GB2312 的大多数程序中,会以两个字节,0xB0(第一个字节)0xA1(第二个字节)储存

代码语言:txt
复制
# python2
>>> u'啊'.encode("gbk")
'\xb0\xa1

世界上的语言实在是太多了,每个都像 GBK 这样搞个字符集可是太让人头大了,而且可能会有冲突。于是出现了 Unicode,中文又称万国码。从名字也可以看出,Unicode 的出现是志向远大,要解决万国语言的编码问题的。目前最新的版本为2020年3月公布的13.0.0,已经收录超过13万个字符。unicode 在几乎所有的语言当中都被支持。在表示一个 Unicode 的字符时,通常会用"U+"(\u)然后紧接着一组十六进制的数字来表示这一个字符。比如 "啊"就可以表示为 '\u554a'.

代码语言:txt
复制
# python2
>>> u"啊"
u'\u554a'
# python2
>>> print(u'\u554a')
啊

# golang 
>>> fmt.Println("\u554a")
啊

上面的例子演示了在 python2 和 golang 中是怎么解析 '\u554a' 这样一个字符串的:他们都会把他们理解为一个 unicode,并且在 print 的时候做另一个对人友好的显示处理,使的人能看到他代表的字符:'啊'。这里可能有人要疑惑了,那么 '啊' 这个子在内存中到底是怎么存在的呢。难道就是 "\u554a"? 其实用下面一个例子可以说明,在大部分点语言中,unicode 表示的字符都用 utf-8 表示【下面会介绍 utf8】,而在 python2 中,他真的是是个 unicode.

代码语言:txt
复制
# golang 
>>> fmt.Println([]byte("啊"))
[229 149 138] 【 == \xe5\x95\x8a 】

# python2
>>> s=u"啊"
>>> s[0]
u'\u554a'

UTF8

需要注意的是,Unicode 只是一个符号集【比如前面介绍的 "\u554a",它只是 unicode 的编码,实际上存储并不一定是这个样子,具体看上面那个例子】,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。比如,汉字严的 Unicode 是十六进制数 4E25,转换成二进制数足足有 15 位(100111000100101),也就是说,这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要 3 个字节或者 4 个字节,甚至更多。

这里就有两个严重的问题:

  • 第一个问题是,如何才能区别 Unicode 和 ASCII ?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?
  • 第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果 Unicode 统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍, 举个例子字符 'A' 使用ASCII 表示只需要1个字节,即 '01000001',而使用 unicode 则需要两个字节 '00000000 01000001',如果更过分,你直接使用 unicode 表示的字符串,则需要六个字节, 即 '\u0041'。

它们造成的结果是:1)出现了 Unicode 的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示 Unicode。2)Unicode 在很长一段时间内无法推广,直到互联网的出现。

于是又出现了目前互联网上最广泛采用的一种Unicode 的实现方式:UTF8。UTF-8 最大的一个特点,就是它是一种变长的编码方式。他是一种针对Unicode的可变长度字符编码,也是一种前缀码。它可以用一至四个字节对Unicode字符集中的所有有效编码点进行编码,属于Unicode标准的一部。【自2009年以来,UTF-8一直是万维网的最主要的编码形式,在所有网页中,UTF-8编码应用率高达94.3%,可以说已经是字符的显示方式的事实标准了】

UTF8 有如下的优点:

  • ASCII是UTF-8的一个子集。即兼容 ASCII
  • UTF-8 和 UTF-16 都是可扩展标记语言文档(XML)的标准编码。所有其它编码都必须通过显式或文本声明来指定。
  • 任何面向字节的字符串搜索算法都可以用于UTF-8的数据。
  • UTF-8字符串可以由一个简单的算法可靠地识别出来。就是,一个字符串在任何其它编码中表现为合法的UTF-8的可能性很低

更多细节可以参考这里

UTF8 与 python

在 python 中,尤其是 python2 中,字符串的处理一直是很令人头疼的问题(愿天堂没有 python2). 根本原因是 python2 的字符串是 ASCII 编码的,也就是说 python 中的一个 string,它只能表示一个 ASCII 编码 的字符串,如果要表示 unicode 字符串怎么办呢,python2 新增了一种类型叫做 unicode, 这种类型,或者类似 u"xxx" 这样的字符串就表示这是一个 unicode 字符串。为了便于 unicode 和 str 之间转换,又有 encode/decode 函数。比如下面的例子.

代码语言:txt
复制
>>> isinstance("", str)
True
>>> isinstance(u"", str)
False
>>> isinstance(u"", unicode)
True
>>> isinstance(u"".encode('utf8'), str)
True

这种处理带来了很多问题,一是带来了编码和存储的混淆,事实上,开发者在编程语言中只需要了解 utf8 就行了,并不需要了解 unicode。因为 unicode只是一种编码,他甚至不是一种存储形式。而 python2 似乎把这一切都搞错了。大量的中文代码中直接实用 unicode 存储,实际上对于一个 unicode 字符使用了6位甚至 更多的长度,严重浪费了存储空间。二是带来了使用的复杂性,应该经历过 python2开发的同学,都曾经被类似这样的错误困扰过吧 UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)

UTF8 与 go

golang 中的字符串和 python3 中比较类似,形式上都是简单的字节数组。字节数组和 string 之间可以简单的 使用 string, []byte 函数进行转换。

golang 中的字符串(注意是 string literals,因为 string value 实际可以包含任意的 bytes)都是 utf8 的,包括代码中定义的字符串。go 中的 string 可以直接转换为 []byte,但是对于 utf8 串,我们在处理的时候往往更关注的是 "character" 即一个一个的字符,而不是 byte。为了解决这个问题,golang 中引入了 rune 的概念,用于表示 "character"。具体看下面这个例子:

代码语言:txt
复制
const nihongo = "日本語"
for index, runeValue := range nihongo {
    // 这里 runeValue 就是一个 rune 类型
    // %#U 会同时打印出 Unicode value 和  printed representation
    fmt.Printf("%#U starts at byte position %d\n", runeValue, index)
}

U+65E5 '日' starts at byte position 0
U+672C '本' starts at byte position 3
U+8A9E '語' starts at byte position 6

golang 中大部分的 unicode、utf8 相关的 library 都在 strconv/quote.go 和 unicode/utf8; unicode/utf16 下面。

代码语言:txt
复制
import (
	"fmt"
	"strconv"
	"unicode/utf8"
)

func main() {
	a := "你好"
	fmt.Println(strconv.QuoteToASCII(a))
	
    for i, w := 0, 0; i < len(a); i += w {
        runeValue, width := utf8.DecodeRuneInString(a[i:])
        fmt.Printf("%#U starts at byte position %d\n", runeValue, i)
        w = width
    }
}

UTF8 与 MySQL

首先补充一下 utf8/ utf16 的表示范围,这里 BMP 可以理解为 unicode 的一层,即一个范围,这个范围里面包含了几乎所有的语言字符,包括很多符号。

代码语言:txt
复制
UTF-8:
    1 byte: Standard ASCII
    2 bytes: Arabic, Hebrew, most European scripts (most notably excluding Georgian)
    3 bytes: BMP
    4 bytes: All Unicode characters
UTF-16:
    2 bytes: BMP
    4 bytes: All Unicode characters

那么 MySQL 的问题来了: MySQL 的"utf8"实际上不是真正的 UTF-8, 而是只包含了 3 个字节以及以下的 utf8, 所以他只是 utf8 的一个子集,对于超过 3 个字节的 utf8 mysql 就无法存储。MySQL 一直没有修复这个问题,在 2010 年发布了一个叫作 "utf8mb4" 的字符集,绕过了这个问题。

这样带来的问题是什么呢,对于大部分的中文字符实际是没问题的,因为大部分中文字符都在两个字符的范围内部,但是对于少部分字符,还有现在很常用的表情符号,MYSQL 的 utf8 就不能存储了。举个例子,😁 的unicode编码为 U+1F601, utf8 表示为 f09f9881,占用了 4个字节。对于这个表情符号 mysql 就不能存储。这也是为什么,对于现代程序,我们应该尽量把默认字符编码设置成 utf8mb4 的原因。

另外,对于已经是 utf8 的数据库了,已经存储了大量数据,更改字符集已经不太现实了,这时候可以怎么办呢。一个做法就是实用 python2 的类似处理,用 unicode 编码的字符表示来存储,即 😁 直接存储为 '\ud83d\ude01' 【这里为什么不是 U+1F601 呢,这两个其实是同一个 16进制数字,d83d+de01 = 1F601 】, 了解了这个,我们其实就可以写一个函数,用于转换大于 3个字符的 utf8 为 unicode 字符表示,下面的函数给出了这个例子【为什么不对 所有非 ASCII 都转换呢,因为2个字符的 utf8 mysql能处理,为什么还要浪费空间呢】。

代码语言:txt
复制
func ConvertStringForMysqlUtf8(s string) string {
	b := strings.Builder{}
	for _, a := range []rune(s) {
		if a < 0x10000 {
			b.WriteRune(a)
		} else {
		    // utf16 对于 bmp 的表示其实就是 unicode 代码
			r1, r2 := utf16.EncodeRune(a)
			v1 := `\u` + strconv.FormatInt(int64(r1), 16)
			v2 := `\u` + strconv.FormatInt(int64(r2), 16)
			b.WriteString(v1)
			b.WriteString(v2)
		}
	}
	return b.String()
}

UTF8 和 JSON

Json 标准中默认大编码为 utf8, 实际在大部分时候无需在意编码的问题,但是使用 python2 另外。因为 python2 的json 库默认会做 ASCII 转义,使得中文或者表情符号被转成 escaped unicode,大量浪费存储空间。对于接收端支持 utf8 的情况,可以大胆设置 ensure_ascii=False

代码语言:txt
复制
# python2
>>> import json
>>> d = {'a': '你好'}
>>> b=json.dumps(d)
>>> print(b)
{"a": "\u4f60\u597d"}
>>> len(b)
21
>>> b=json.dumps(d, ensure_ascii=False)
>>> print(b)
{"a": "你好"}
>>> len(b)
15

另外值得注意的是,这种 escaped unicode 的表示,在其他语言接受的时候可能会被转换成 utf8 string, 当再次 marshal 成为 string 的时候,你会发现,和接受到的 string 不一样了。举个例子:

代码语言:txt
复制
// golang
import (
	"fmt"
	"encoding/json"
)

func main() {
	a := []byte(`{"a": "\u4f60\u597d"}`) // {"a":"你好"}
	fmt.Println("before --> len:", len(a), "json:", string(a))
	b := map[string]interface{}{}
	_ = json.Unmarshal(a, &b)
	c, _ := json.Marshal(b)
	fmt.Println("after ---> len:", len(c), "json:", string(c))
}


>> before --> len: 21 json: {"a": "\u4f60\u597d"}
>> after ---> len: 14 json: {"a":"你好"}

接收到 json 在 unmarshal 的过程中,已经把json 中的 escape unicode 转成了 utf8,再 marshal 也都是 utf8了,完全没有再转成 escape unicode 的必要。

这里有个相关的有趣的 ISSUE, 为什么 golang 就不能 marshal with unicode escape 呢 😁

另外在 golang bson 的 json unmarshal 中,发现了一个和 mysql utf8 很类似的问题,即这个库也不能识别大于 3 个字符的 unicode,这个问题现在还没被修复。

代码语言:txt
复制
import (
"code.byted.org/bytedoc/mongo-go-driver/bson"
)
// golang
func main() {
	a := `{"a" : "\ud83d\udea5"}` // 🚥
	var c map[string]interface{}
	_ = bson.UnmarshalExtJSON([]byte(a), false, &c)
	fmt.Println(c)
}

>> map[a:��]

参考

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 数字
  • AscII 码
  • Unicode 编码
  • UTF8
  • UTF8 与 python
  • UTF8 与 go
  • UTF8 与 MySQL
  • UTF8 和 JSON
  • 参考
相关产品与服务
云数据库 MySQL
腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档