项目中的一个问题
最近项目中使用 Python包中的 json.dumps()
函数将 Python 对象转换为 JSON 格式的字符串时,如果该对象中包含中文字符,可能会出现中文乱码的问题。这是因为 json.dumps()
默认使用 ASCII 编码来编码字符串,而 ASCII 编码不支持中文字符,因此会出现乱码。
要解决这个问题,可以将 ensure_ascii
参数设置为 False
,这样 json.dumps()
函数会使用 UTF-8
编码来编码字符串,从而支持中文字符。示例如下:
import json
data = {"name": "程序锅", "age": 30}
json_str = json.dumps(data, ensure_ascii=False)
print(json_str)
输出结果为:
{"name": "程序锅", "age": 30}
问题是解决了。不过我们在日常编程的过程中,时不时都会遇到编码错误。。什么ASCII码呀、UTF-8呀、GBK等等。每次报错了只能去网上搜,不知其中的原理是什么。正好借着这个json.dumps()
中文乱码这个问题,聊一聊字符编码那件事。
在聊字符编码前,我们需要了解清楚为什么要编码。
因为我们的计算机只能够识别01这样的数字。你输入一个字符串,比如hello
,需要通过字符编码,将字符串转化为计算机能够识别的01数字串。
ASCII编码
大家如果要想互相通信而不造成混乱,那么大家就必须使用相同的编码规则。因为计算机是美国搞的,美国有关的标准化组织就出台了ASCII编码
,统一规定了上述常用符号用哪些二进制数来表示。
需要注意的是ASCII编码,只涉及128个常用的英文字符,并不涉及中文、法文等其他语言。
其中ASCII编码见下图1。
hello
用ASCII编码的结果为:h对应ASCII值为104
,用二进制表示为01101000
,用十六进制表示为68
,其余几个字母以此类推(对应关系见图1)。
不过上面也说到,ASCII编码只涉及128个常用的英文字符。2的7次方为128,因此我只需要7位就能表示全部128个常用英文字符。然后1字节=8位,ASCII编还多出1位。
所以在ASCII编码中,也约定ASCII编码的最高位为0。
不过ASCII编码出来后,也存在很多问题。
ASCII编码存在位数不足的问题
在英语中,用128个符号编码便可以表示所有。但是其他语言128个符号是不够的。比如,在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。
但是,这里又出现了新的问题。不同的国家有不同的字母。因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel(ג),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0-127表示的符号是一样的,不一样的只是128-255的这一段。
至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示 256^2 =65536个符号。
所以ASCII编码存在位数不足的问题,需要引入Unicode编码。
Unicode编码
可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是Unicode
,就像它的名字都表示的,这是一种所有符号的编码。
Unicode当然是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+9505表示汉字“锅”。具体的符号对应表,可以查询Unicode.org,或者专门的汉字对应表。
但需要注意的是
Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。
有人就会问,二进制代码都确定了,存储不就按照二进制代码来就可以了?
粗想貌似是可以的
不过细想后这里就有两个严重的问题。
第一个问题是,如何才能区别Unicode和ascii?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?
第二个问题是,我们已经知道,英文字母只用一个字节表示就够了。如果Unicode统一规定,每个符号固定用三个或四个字节表示,这样就不存在弄不清楚三个字节表示一个符号,还是分别表示三个符号,第一个问题貌似解决了。不过这样每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。
UTF-8与Unicode编码之间的关系
所以随着互联网的普及,强烈要求出现一种统一的编码方式用于实现存储Unicode。
UTF-8就是在互联网上使用最广的一种Unicode的实现方式。其他实现方式还包括UTF-16和UTF-32,不过在互联网上基本不用。
重复一遍,这里的关系是,UTF-8是Unicode的实现方式之一
Unicode表示我的编码已经确定好了,UTF-8需要寻找一种方式去解决这个编码的存储问题
所以UTF-8是干啥了呢?具体Unicode转UTF-8的方式如下表所示。
UTF-8的编码规则很简单,只有二条:
1.对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
2.对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
下表总结了编码规则,字母x表示可用编码的位。
Unicode符号范围(十六进制) | UTF-8编码方式(二进制) |
---|---|
0000 0000-0000 007F | 0xxxxxxx |
0000 0080-0000 07FF | 110xxxxx 10xxxxxx |
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
举个例子,已知“锅”的Unicode
是9505(1001010100000101)
,根据上表,可以发现9505
处在第三行的范围内(0000 0800-0000 FFFF),因此“锅”的UTF-8
编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx
。然后,从“锅”的Unicode
编码最后一个二进制位开始,依次从后向前填入UTF-8
格式中的x,多出的位补0。这样就得到了,“锅”的UTF-8编码是11101001 10010100 10000101
,转换成十六进制就是E99485
。
用python代码验证:
str = "锅"
print(f"{str}的Unicode编码为:{hex(ord(str))}")
print(f"{str}的UTF-8编码为:{str.encode('UTF-8')}")
结果如下:
最后
UTF-8只是Unicode实现的一种方式,还有UTF-16、UTF-32等等。前面有提到UTF-8的编码规则,大家也可以思考为什么要这样编码,比如Unicode编码:0000 0080-0000 07FF
为什么就对应UTF-8编码:110xxxxx 10xxxxxx
,UTF-8有哪些优势和劣势,也欢迎大家私信或者留言和我讨论。
参考:
1.https://blog.csdn.net/qq_43043859/article/details/89510121
2.https://zhuanlan.zhihu.com/p/27364614?utm_id=0