字符编码在我的日常工作中默默地起着至关重要的作用,因此搞清楚它的原理是很重要的。
背景
Unicode
与utf-8
是平时工作几乎每天都会接触到的内容,但是从来没有总结过。- 之前在使用python2的时候忘记在中文字符串前面加u而导致了如下报错,因此想探究一下字符串底层的编码原理。
SyntaxError: Non-ASCII character '\xe4' in file script.py on line 3, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details
- 在使用
go
语言的过程中产生了一个疑问,byte
和rune
的意义是什么? Unicode
一共才十几万个字符,为啥utf-8
最多需要4个字节,3个字节应该就足够用了呀?
所以想写个文章总结一下,等到以后再迷糊了可以回来看看,毕竟自己的记性不大好。
字符与字节
字符就是我们平时看到的“文字”、“数字”、“标点符号”
等具有特殊意义的标识。一个汉字
、一个希腊字母
、一个英文字母
等都是一个字符。
字节是计算机术语,一个字节是8bit。一个字节只能表示一个数字,这个数字的范围是0~256. 因为计算机只认识0和1,因此与计算机有关的存储都是通过字节来实现的。至于非数字的字符,通过存储一个与之一一对应的数字来实现。
Unicode
因为最早计算机是说英语的人发明的,所以他们制定了一个所有英文字符与数字的一一对应关系,这个关系表就叫做ASCII
表。后来计算机在全球得到了普及,显然ASCII
表不够用了,我们需要一个更大的表。
Unicode
就是这个“更大的表”,因此他其实就是一个字符集,我么可以认为他包含了世界上所有国家、所有语言中的字符,并将这些字符和一个数字建立了一个唯一的映射关系,这个映射关系完全兼容ASCII
表,也就是说非英文字符全排在了英文字符的后面。根据维基百科
的资料显示,截止2024年9月已经收录了154,998个字符。
utf-8
英文字符用1个字节就能表示,其他的字符有的用2字节就可以,有的用3字节就可以,有的用4字节。具体是根据你在unicode表里的数值决定的,下图就是数值范围与字节格式之间的关系。汉字一般都是在第3条规则范围里面。
可以看到utf-8
是一种“勤俭节约”的方法,但是也会有一个问题。
比如下面三个字符的unicode二进制表示为
- “你” 00011100 01011100 用两个字节表示
- “1” 10001100 用一个字节表示
- “我” 10001100 00011100 01011100 用三个字节表示
那么当遇到10001100 00011100 01011100的时候是解析成“我
“还是“1你
“?
utf-8编码规则规定了当读到一个字符的第一个字节的时候回根据第一个字节的标志位决定再向后读取几个字节来表示当前的字符。解读 UTF-8 编码非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节
最后
回答一下最初的4个问题吧
Unicode
是个字符集,utf-8
是Unicode
的一种“省空间”的编码方式,除了utf-8
还有utf-16
等其他的编码方式- python2默认字符串是使用byte编码的,也就是一个字节就代表一个字符,因此只认识
ASCII
中的字符,遇到诸如汉字这种使用utf-8
编码的Unicode
字符的时候就“不认识了” - golang中字符串底层是“字节流”,使用
[]byte
存储,[]rune
是对[]byte
使用utf-8
编码方式的抽象
func main() {
str := "hello世界"
fmt.Println(str)
fmt.Println([]byte(str))
fmt.Println([]rune(str))
}
hello世界
[104 101 108 108 111 228 184 150 231 149 140]
[104 101 108 108 111 19990 30028]
很明显两种方式对于前5个字符“hello“的编码是一样的,这也验证了Unicode
是完全兼容ASCII
的说法。不一样的地方在于对于“世界”这2个字符,下面就以“世”这个字符做例子看看转换规则。它在Unicode
字符集中对应的数字是19990,二进制为:
100 111000 010110
对应的utf-8
的编码规则是“3个字节的规则“,这也是为啥把上面的2进制序列分成3段的原因
1110xxxx 10xxxxxx 10xxxxxx
填充过程是从后向前的
第一步:将010110
填充到最右边的10xxxxxx
,就变成了
1110xxxx 10xxxxxx 10010110
第二步:将111000
填充到中间的10xxxxxx
,就成变了
1110xxxx 10111000 10010110
第三步: 将100
填充到最右边的1110xxxx
,不够就补0,注意填充是从后向前,所以如果需要补0那么肯定是在最前面补,最终的结果
11100100 10111000 10010110
这3个字节代表的数字就是228,184,250,与[]byte
输出的结果是一样的
4. 因为utf-8
编码规则有的位是用来标识的,并不是所有的都用来存储的