为什么要写这篇文章呢?这里就要提到某一天,工作的时候,突然发现自己在编码方面,一窍不通。实在惭愧
字符编码是计算机技术的基石,对于程序员来说尤其重要,字符编码的知识是必须要懂的
编码入门知识
ASCII 码:计算机内部,所有信息最终都是一个二进制值。每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从00000000到11111111。上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为 ASCII 码,一直沿用至今。ASCII 码一共规定了128个字符的编码,比如空格SPACE是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的一位统一规定为0
非ASCII 码编码:英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。比如,在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号
至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示 256 x 256 = 65536 个符号
世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样
Unicode:Unicode 当然是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+4E25表示汉字严。具体的符号对应表,可以查询unicode.org,或者专门的汉字对应表。Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。
比如,汉字严的 Unicode 是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说,这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多
这里就有两个严重的问题,第一个问题是,如何才能区别 Unicode 和 ASCII ?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果 Unicode 统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。
它们造成的结果是:1)出现了 Unicode 的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示 Unicode。2)Unicode 在很长一段时间内无法推广,直到互联网的出现
UTF-8/16/32:UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式。其他实现方式还包括 UTF-16(字符用两个字节或四个字节表示)和 UTF-32(字符用四个字节表示),不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8 是 Unicode 的实现方式之一。UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度
UTF-8 的编码规则很简单,只有二条:
1)对于单字节的符号:字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的;
2)对于n字节的符号(n > 1):第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
跟据上表,解读 UTF-8 编码非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。
下面,还是以汉字严为例,演示如何实现 UTF-8 编码。
严的 Unicode 是4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800 - 0000 FFFF),因此严的 UTF-8 编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx。然后,从严的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,严的 UTF-8 编码是11100100 10111000 10100101,转换成十六进制就是E4B8A5
GB 18030(系列):与UTF-8相同,采用多字节编码,每个字可以由1个、2个或4个字节组成;
编码空间庞大,最多可定义161万个字符;
支持中国国内少数民族的文字,不需要动用造字区;
汉字收录范围包含繁体汉字以及日韩汉字
BIG5字符集&编码:Big5,又称为大五码或五大码,是使用繁体中文(正体中文)社区中最常用的电脑汉字字符集标准,共收录13,060个汉字
1)ANSI是默认的编码方式:对于英文文件是ASCII编码,对于简体中文文件是GB2312编码(只针对 Windows 简体中文版,如果是繁体中文版会采用 Big5 码);
2)Unicode编码这里指的是notepad.exe使用的 UCS-2 编码方式:即直接用两个字节存入字符的 Unicode 码,这个选项用的 little endian 格式
3)Unicode big endian编码与上一个选项相对应
4)UTF-8编码
即GBK、GB2312等与UTF8之间都必须通过Unicode编码才能相互转换:
1)GBK、GB2312 --先转–> Unicode --再转–> UTF8
2)UTF8 --先转–> Unicode --再转–> GBK、GB2312
Base64 编码
现有的字符集非常多, 常用的有 UTF-8 / GBK 等,这里面的某些字节在某些传输渠道不支持,比如邮件传输不支持上面ASCII码的控制字符,Base64 的创建就是为了解决此问题
Base64就是为了解决各系统以及传输协议中二进制不兼容的问题而生的
Base64有64个字符, 2^6 = 64, 所以每个Base64编码字符可以用一个6位的二进制来表示.
使用 Base64 进行编码,大致可以分为 4 步:
- 将原始数据每三个字节作为一组,每个字节是8个bit,所以一共是 24 个 bit
- 将 24 个 bit 分为四组,每组 6 个 bit
- 在每组前面加补 00,将其补全成四组8个bit
到此步,原生数据的3个字节已经变成4个字节了,增大了将近30% - 根据Base64码表得到扩展后每个字节的对应符号
有时我们会在Base64字符末尾会看到=,有时1个,有时2个,这是为啥?
通过上面的我们知道了Base64编码过程是3个字符一组的进行,如果原文长度不是3的倍数怎么办呢? 例如我们的原文为Ma,它不够3个,那么只能在编码后的字符串中补=了。缺一个字符补一个,缺两个补两个即可,所以有时候你会看见base64字符串结尾有1个或者2个=。
Python 2.x
Python 2.x 版本中,默认的编码方式是 ASCII
所以在Python 2.x 版本当中,如果需要加中文注释,都需要添加以下代码,以进行编码声明
# -*- coding: utf-8 -*-
json 编码注意事项:需要显示的添加encoding='utf-8'
,并且设置添加ensure_ascii=False
参数
import json
all_res = {}
write_path = "test.json"
all_res["名字"] = "段誉"
with open(write_path, "w", encoding='utf-8') as f:
json.dump(all_res, f, ensure_ascii=False)
# test.json 内容
{"名字": "段誉"}
Python 3.x
Python 3.x 版本中,字符串默认使用unicode 编码
和前面提到一样,unicode是中间编码,任何字符编码之前的转换都必须解码成unicode,再编码成目标字符编码
json 编码注意事项:需要显示的设置添加ensure_ascii=False参数
如果dict包含有汉字,一定加上ensure_ascii=False。否则按参数默认值True,意思是保证dumps之后的结果里所有的字符都能够被ascii表示,汉字在ascii的字符集里面,因此经过dumps以后的str里,汉字会变成对应的unicode
import json
all_res = {}
write_path = "test.json"
all_res["名字"] = "段誉"
with open(write_path, "w") as f:
json.dump(all_res, f, ensure_ascii=False)
# test.json 内容
{"名字": "段誉"}
总结
任何编码转换之前都要解码成unicode,再转换到目标编码
u 开头的是unicode 编码
b 开头的是byte
Python3的str 默认不是bytes,所以不能decode,只能先encode转为bytes,再decode
Python2的str 默认是bytes,所以能decode
Python 2.x 的编码注意规则
# -*- coding: utf-8 -*-
utf_8_a = '中文'
gbk_a = utf_8_a.decode('utf-8').encode('gbk')
print(gbk_a.decode('gbk'))
#输出结果: 中文
疑问例子
>>> a = {"data" : "你是谁\035我是段誉"}
>>> a # # 在输出字典 a 时,Python 使用\x1d 来表示\035,这是十六进制表示
{'data': '你是谁\x1d我是段誉'}
>>> b = json.dumps(a)
>>> b # 其中 \035 => unicode 编码为 \\u001d
'{"data": "\\u4f60\\u662f\\u8c01\\u001d\\u6211\\u662f\\u6bb5\\u8a89"}'
>>> c = json.dumps(a, ensure_ascii=False)
>>> c
'{"data": "你是谁\\u001d我是段誉"}'
>>> e.encode("utf-8").decode("unicode_escape")
'{"data": "你是谁4我是段誉"}'
解答一:变量b,json.dumps 成str 之后,无法转回中文
unicode_escape 将unicode 的内存编码进行存储,读取文件的时候反向转换回来(直接将unicode 编码进行转换)
解答二:\035 的问题
对于 \035 编码问题的解释,\035 在 ASCII 码中如图所示,8 进制表示\035,\x1d 表示16 进制,属于不可见字符( < 32 都属于不可见字符)
当你使用 json.dumps(a) 对字典进行 JSON 编码时,默认情况下,Python 将特殊字符进行 Unicode 转义,以确保生成的 JSON 字符串是 ASCII 可读的。这就是为什么在输出的 JSON 字符串中看到 \u001d,它是 Unicode 编码的表示形式,对应分组符
但是为什么以上例子中c变量还是输出\u001d,因为特殊字符’\035’属于不可见字符(ASCII 码 < 32)所以仍然会转成unicode编码\u001d,这是json 规范的一部分,确保生成的json 字符串是有效的可移植的。
如果这个符号是\064->4,的话那就是c=“{“data”: “你是谁4我是段誉”}”
即以为在media_type 为json 时候:用来告诉服务端,消息主体是序列化后的JSON字符串(默认会转unicode 编码)
media_type 为 plain 时候:纯文本格式,空格转换为 “+” 加号,但不对特殊字符编码
参考
https://zhuanlan.zhihu.com/p/38333902
https://zhuanlan.zhihu.com/p/384238870
https://blog.csdn.net/j550341130/article/details/100046887