python对字符编码的处理(原理篇)

一些关于终端的实验

首先先做个小实验,回答上篇两个简单的问题:

  1. 文件读写接口的具体不同?
  2. 文本分段fwrite,会不会乱码?
#include "stdafx.h"
#include<stdio.h>
#include <string.h>
#define INPUT_MAX 8

int _tmain(int argc, _TCHAR* argv[])
{
	int input_len;
	int read_len = 100;

	char buffer[1024];
	char *data;
	char *prompt = ">>> ";

	fprintf(stdout, "%s", prompt);
	while ((data = fgets(buffer, read_len, stdin)) != NULL)
	{
		input_len = strchr(data, '\0') - data;
		fprintf(stdout, "get your input!\n");
		fprintf(stdout, "共输入%d个字符,它们是:", input_len);

		while (input_len > INPUT_MAX) {
			const int chunk_size = INPUT_MAX;
			fwrite(data, 1, chunk_size, stdout);
			data += chunk_size;
			input_len -= chunk_size;
		}
		fwrite(data, 1, input_len, stdout);

		fprintf(stdout, "逐字节输出:");
		for (data = buffer; *data != '\0'; data++)
		{
			fputc(*data, stdout);
		}

		fprintf(stdout, "%s", prompt);
		
	}
	return 0;
}

在这里插入图片描述
这么一看,fprintf fwrite fputc 均支持中文显示。
所以在python中,尽管我们看到变量名+回车(repr)和print(str)两种方式的结果不同,看到了调用接口前者是fprintf+fputc,后者是fwrite,也不能想当然得认为输出接口的改变是造成打印差异的原因。
所以,还是回到逻辑层,我们得知这个差异其实是打印时指定 Py_PRINT_RAW 的锅。到底什么才是加工后输出,什么才是原生输出(Py_PRINT_RAW)?

这里简单总结:
变量名+回车方式(repr),加工后输出,倾向于把文本当成二进制数据看待,将编码显示出来,展示的是文本在内存中的存储方式。
原生输出方式(str),Py_PRINT_RAW,倾向于易于人类语言的方式,展示的是文本在终端设备上的显示方式。

至于将数据分批写入是否乱码,实验中fwrite和fputc都给出了答案,标准输出stdout的缓冲区解决了这个问题。说到这个,会想到用fflush来冲洗缓冲区来看看字符会不会分节,试过之后依然是失败的,感觉操作系统底层或许有什么保护机制。那至少我们知道控制台的字符编码是cp936,在这个终端上,我们输入的数据是cp936,输出也是cp936,理论上怎么搞都不会乱码的。果断我把中文存到gbk编码的txt中,把终端的默认编码改成如下,得到了我要的效果。
在这里插入图片描述
在这里插入图片描述
写到这里,乱码的原因有点眉目了,相连的文本数据只要没被破坏,哪怕分段输出也没问题。
但是,文本数据的编码,和终端设备(标准输出、浏览器、文件编辑器、SecureCRT控制台等)的显示字符编码,如果不一致,就会乱码。
我们控制台是cp936,尝试把它转成utf8打印,就乱码了。
在这里插入图片描述
果然如此,有点小激动,感觉我们快接近问题的核心了。
但是不要急,我们需要补充一些历史知识(冷知识):这个代码页是什么鬼?

代码页

code page,刚好对应我们常看到的cp936的前缀。根据维基百科的解释,代码页是对字符编码的古老的编号,主要是IBM、微软两家公司提出的规范。所以它对目前流行的的字符编码名称有映射关系,python中{PythonDir}\Lib\encodings\aliases.py 中的那个大表,就是这么来的。

代码页936(Codepage 936)是Microsoft的简体中文字符集标准。

aliases是“别名”的意思。 可想而知,在互联网诞生之前,字符编码没有统一规范的时候,世界各地的字符编码是多么的混乱不堪。 我们从aliases这张表能看出,一些地区的字符编码逐渐统一成了一张字符集,旧的名字被新的名字所替换。cp936就是gbk,或者换句话说,cp936字符集和gbk的字符集是一样的,给你一堆数字,告诉你它是cp936/gbk,你就能在字符集里查到这个数字对应的中文长啥样。

Unicode

顺藤摸瓜,我们也补一下字符编码的冷知识吧。我们需要知道的,字符集就像有版本号一样,内容是不断扩充的,新的版本包含旧的版本。比如 gb2312 < gbk < gb18030。
在这里插入图片描述
在这里插入图片描述
关于unicode,它像是一个通用数据结构,任何字符编码都可以通过一定规则换算成unicode编码,这种换算就是decode(解码、译码)。前面我们花了很多时间理解 gbk <-> unicode 之间的转换,可能还是不太能记住哪个才是encode,哪个才是decode。这里有个方法:

因为unicode是通用的编码方式,就好比是个通用世界语言一样,你可以理解成是公文。gbk的编码,实际上是服务于中文简体用户的(记住它对应一个简体中文字符集),对于其他语言的用户来说他们就读不懂了,就像是密文。所以:
gbk -> unicode = 解密 = decode
unicode -> gbk = 加密 = encode
在这里插入图片描述
还有一点要注意,unicode从拼写上看,union of code,是个概念、口号、通用字符编码规则,并不是某个具体的字符编码,它没有对应的字符集,unicode对象仅仅意味着,这个对象对应的内存中的二进制数据是字符串,它是哪个地区的语言,对应哪个编码,该怎么显示,并不知道。所以你看不到终端设备将字符串以unicode形式输出的(除非你是要那种加工的方式),所以python在print unicode的时候,它知道要从标准输出取其对应的字符编码,转码(encode)之后再输出,这样的输出才是人类语言。那你可能要问了,unicode有啥用呢?

它解决了一个关键问题:字符码(Code Point)和字符的一对一关系,这样我们就能正常获取字符长度,做字符串切割和拼接也不在话下。 ‘汉’字在gbk中是2个字节,在utf8中是3个字节,len返回的是字节数,而非字符数。
在这里插入图片描述
这就是str和unicode之间最重要的区别。尽管unicode的编码也是2个字节,但是unicode知道这2个字节是一起的,其他字符编码就做不到这个。

在这里插入图片描述
这个口号喊出来了,UTC就提出个标准:
USC-2:每个字符用2个字节表示。然后开始研发一张超大的字符集,utf16。
后来他们担心2个字节不够用,又提出了一个新标准:
USC-4:每个字符用4个字节表示。对应的字符编码utf32。

因为这两个规范都是hard code字节数。对于最简单的ascii,会有大量的无意义的0,对于存储、网络传输都是极大的浪费,于是就有了utf8,用变长字节数表示一个字符。即头字节中读取到连续的n个1B(一直读到0为止),那包括该字节在内一共有n个字节组合起来的二进制数据,表示一个字符。 在规范中,后续的n-1个字节都必须是10B开头(剩下6位才是有效的数据位)。这种数据头(head)的设计方式,就非常适用于网络传输了。 我们最多可以有7个字节表示一个字符,最多可以有 (2^((7-1)*6)-1=) 68719476735 个字符纳入编码中,但实际情况,utf8只用了1-4个字节来表示字符。

head = 1111 1110
body = 1011 1111 1011 1111 1011 1111 1011 1111 1011 1111 1011 1111

Unicode 和 utf8 之间的转换

这里以汉字为例
在这里插入图片描述
unicode 0110 1100 0100 1001
utf8:
head = 1110 0110 //前面3个1,表示有3个字节表示这个字符
body = 1011 0001 1000 1001 //将加粗部分拼接起来,就得到unicode

具体规则看下表
在这里插入图片描述
面对gbk字符串的网络传输,我们可能要这样做

发送端:
msg = a.decode(‘gbk’).encode(‘utf8’)
sendmsg(msg)

接收端:
msg = recv()
a = msg.decode(‘utf8’).decode(‘gbk’)

也就是说,当我们面对乱码时,应该聚焦到这两个问题:

  1. 终端的字符编码格式是什么?
    上文已移到,去设置里查。

  2. 字符串的原始字符编码是什么?这些子问题
    2.1 怎么知道字符串的字符编码?
    站在网络传输的接收端,按照utf8的规则能正常接收数据。但是传过来的不是utf8编码的字符串呢?这个问题似乎很难解决。
    2.2 怎么知道文件的字符编码?
    文件保存时有文件头、文件未的数据,可以用于辅助判断。

文件的字符编码检测

相关工具 chardet cchardet
https://pypi.org/project/chardet/
https://pypi.org/project/cchardet/

字符编码相关的模块 codecs

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值