数字
在计算机中,所有的信息最终只会表现为 0 和 1,所以计算机看到的数据是这样的。
00010101010001001000110001100101010101
那么就带来一个问题,怎么用二进制来表示我们程序中需要使用的信息呢,比如 数字、字符、表情等等。
本文福利, 免费领取C++学习资料包、技术视频/代码,1000道大厂面试题,内容包括(C++基础,网络编程,数据库,中间件,后端开发,音视频开发,Qt开发)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓
首先数字的问题比较好解决。把特定长度 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 码和 对应字符的转换的,比如下面的例子
>>> chr(65)
'A'
>>> chr(97)
'a'
>>> ord('A')
65
Unicode 编码
AscII 编码固然简单,但是只能解决英语世界的字母问题,用于表示中文等其他语言,显然是不够的。所以,中国制定了 GB2312 编码,用来把中文编进去。GB/T 2312标准共收录6763个汉字,其中一级汉字3755个,二级汉字3008个。每个汉字及符号以两个字节来表示。GBK 可以看成是 GB2312 的兼容扩展,共收入 21886 个汉字和图形符号。举个例子:"啊" 在 支持 GBK/GB2312 的大多数程序中,会以两个字节,0xB0(第一个字节)0xA1(第二个字节)储存
# python2
>>> u'啊'.encode("gbk")
'\xb0\xa1
世界上的语言实在是太多了,每个都像 GBK 这样搞个字符集可是太让人头大了,而且可能会有冲突。于是出现了 Unicode,中文又称万国码。从名字也可以看出,Unicode 的出现是志向远大,要解决万国语言的编码问题的。目前最新的版本为2020年3月公布的13.0.0,已经收录超过13万个字符。unicode 在几乎所有的语言当中都被支持。在表示一个 Unicode 的字符时,通常会用"U+"(\u)然后紧接着一组十六进制的数字来表示这一个字符。比如 "啊"就可以表示为 '\u554a'.
# python2
>>> u"啊"
u'\u554a'
# python2
>>> print(u'\u554a')
啊
# golang
>>> fmt.Println("\u554a")
啊
上面的例子演示了在 python2 和 golang 中是怎么解析 '\u554a' 这样一个字符串的:他们都会把他们理解为一个 unicode,并且在 print 的时候做另一个对人友好的显示处理,使的人能看到他代表的字符:'啊'。这里可能有人要疑惑了,那么 '啊' 这个子在内存中到底是怎么存在的呢。难道就是 "\u554a"? 其实用下面一个例子可以说明,在大部分点语言中,unicode 表示的字符都用 utf-8 表示【下面会介绍 utf8】,而在 python2 中,他真的是是个 unicode.
# 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 函数。比如下面的例子.
>>> 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"。具体看下面这个例子:
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 下面。
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 的一层,即一个范围,这个范围里面包含了几乎所有的语言字符,包括很多符号。
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能处理,为什么还要浪费空间呢】。
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
# 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 不一样了。举个例子:
// 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,这个问题现在还没被修复。
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:��]
本文福利, 免费领取C++学习资料包、技术视频/代码,1000道大厂面试题,内容包括(C++基础,网络编程,数据库,中间件,后端开发,音视频开发,Qt开发)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓