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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章