編程入門之字符編碼與亂碼


  ——“為什么服務器收到的請求或者打開的文本文件有時會亂碼?”

  ——“因為編碼不對。”

  ——“編碼的本質是什么?為什么編碼不對就會亂碼?一段文本是如何在網絡中傳輸后最終顯示給用戶的?Java String默認使用什么編碼?”

  ——“……”

  亂碼問題相信很多同學都有幸遇到過的,也解決過,但根據個人面試的經驗,對該問題知其然亦知其所以然的同學,是少之又少的。故在這里做一下分享,以備在其他的面試中被問到:-)。

  因為計算機已經發明很久了,“不要重復發明輪子”也是一句大家耳熟能詳的古訓,我們已經習慣了編寫Print("A"),就會在屏幕上顯示一個字符A的便利,認為這一切自然而然。而過程中需要哪些支持,發生了什么,思考的人已經越來越少了。下面我們推理下在輪子還不那么齊全的年代,如何實現一個顯示字符的“記事本”程序。

  一、文本的存儲

  .txt文件非常常見,當我們在windows桌面右鍵新建一個“文本文檔”,在其中輸入A之后保存,就在桌面形成了一個保存着A的文本文檔A。然后我們雙擊它打開,就會看到這個保存的A。

  

  而學校里的課程告訴我們,計算機中存儲的都是0和1這種2進制數據,無法存儲“A”,那磁盤存儲的究竟是什么?我們換另外一類工具來打開這個文本文檔,這類工具叫做16進制編輯器,這里使用HxD編輯器。顯示如下內容。

  

   這里顯示了實際的存儲內容為一個字節0x41,對應的文本是A。 這時我們在41這個字節之后輸入一個字節0x42,這時對應的文本顯示了B

  

  保存后用記事本打開這個存儲了兩個字節的文件,同樣會顯示AB,即我們通過輸入0x42的方式,輸入了字符B。

    

  二、字符集

  上面揭示了,我在記事本程序中輸入字符A后保存,存儲在磁盤文件的實際是數字0x41(對應二進制0100 0001),而如果我在16進制編輯器中直接追加一個0x42,則用記事本打開會顯示B。所以記事本程序一定有一個轉換功能,這個轉換規則可能是輸入一個字符A,則轉換存儲為0x41。反之讀取時,如果是0x41則顯示字符A,如果是0x42則顯示B,其實可以理解為一個保存編碼,顯示解碼過程。顯然字母有26個,算上大小寫可能有27個,再加上些加減乘除,愛心符號,所以我們需要全面的定義這種對應關系,對常用的字符定義完成后,可能會得到下面這樣一張表,就是傳說中的ascii字符集。

  

  這張表定義了字符與計算機存儲二進制數據間的對應關系,因此要實現記事本程序,本質上是實現了一個將二進制與可見字符轉換的程序。當輸入字符時保存為二進制,當讀取二進制時,轉換為字符顯示。是不是看上去簡單的記事本比想象中的略復雜了些。但字符集是抽象的。所謂抽象是指定義了字符的編碼之后,仍然無法在屏幕上顯示出一個字符A。接下來需要考慮,要把一個字符A顯示在屏幕上,需要做哪些具體的工作。   

  三、字庫(字體)

  屏幕上顯示的A實際上是一個圖形,顯示A的過程,本質上是需要在屏幕上畫出一個形狀為A的圖形。並且A的寫法有很多種,如下面都是A。因此我們需要具體定義出當我們要顯示字符A的時候需要把A繪制成什么樣子,當然還要同樣定義B,C,D等。

   這個定義我可以硬編碼在我的“記事本程序”中,那這個定義就是一種私有的定義,保存的文件拿到其他的文本編輯器,就無法正確的顯示出我保存的A的樣子了。因為其他程序繪制A的方式也許不同。一個比較好的主意是把這個定義公開出來,定義一個標准格式,這樣大家都可能解析,編制這個定義字符形狀的文件,可以保證顯示的通用一致性,這個就叫做字體文件。

  實踐中字符集定義和字體文件的定義都是標准公開的,這樣系統內的程序都可以將相同的文件內容對應成相同的字符,如果願意,也使用相同的字體來顯示,保持風格的一致。

  字符形狀(字體)的定義無疑包含至少包含兩個要素,這個字符的圖形和這個字符的索引編碼。程序需要繪制字符A的時候可以用編碼0x41,去字體文件搜索對應的字體定義,然后調用其他的繪圖API把A“畫”出來,繪圖API可以理解為一些比較底層的繪圖方法,實現類似將第一排第一列的像素點顯示為黑色這樣的功能,驅動顯示芯片在顯示器上繪圖。

  像素字體是一種符合直覺的定義方式

  

  按照定義將其繪制到對應屏幕像素點上,就形成了文字。當然問題是縮放可能會比較模糊,定義時可能會加入字號信息,為不同的字號,定義一系列不同的像素點陣,改善顯示效果。高級的做法也容易想到,就是用數學描述的方式來定義字符形狀,形成所謂矢量字體,優點可以無極縮放,缺點可能是需要繪制邏輯比較復雜,對資源占用高。

  四、亂碼的產生

  有了以上的背景知識,推而廣之就容易想到亂碼是如何產生的了。亂碼的產生本質是由於“記事本”之類的程序,對文件的二進制內容無法正確轉換為字符進行繪制而產生。

  如果全世界只存在ascii一種字符集則簡單的多。但因為世界范圍內的語言文字眾多,除了英文字母外,還有中文,希臘文,日文……。這些文字符號也有被計算機存儲顯示的要求,如我國會有顯示中文的需求,因此會存在眾多的字符集,通常是以國家區域推行,自己的事情自己操心嘛。於是可能存在這樣的定義。

  如在某字符集編碼中約定(GBK編碼集)

  兩個字節 0xD6 0xD0  對應字符   “中”

  我們使用了一個強大的支持中文編輯的“中文記事本”輸入了一個“中"保存了起來。 實際存儲內容為 0xD6 0xD0。 此時我們用上面那個“功能簡單的記事本”讀取顯示該文件,假設它只支持ascii編碼集, 那他會逐個字節對文件內容進行處理顯示,讀取第一個字節0xD6去ascii編碼集中尋找對應的字符進行顯示,之后讀取0xD0進行顯示,於是變成了下面這樣的所謂“亂碼”。由此可見,亂碼的原因,可能是對應了錯誤的字符,或者對應不可見字符,或者根本就不存在的字符如何處理顯示取決於程序自身的處理。

  

  思考題:但為啥實際中的windows記事本是可以記錄中文的,為何它打開0xD6 0xD0 會知道是以GBK編碼保存的呢?:-)

  當然很久很久之后,分久必合,自然而然產生了unicode編碼,即統一碼,可以以一套編碼編碼全世界所有的語言文字符號。避免了編碼各自(各國各民族)為戰的情況。

  五、總結

  •   計算機文件的存儲及網絡傳輸都是基於二進制數據流進行的。
  •   亂碼現象是由於輸入存儲(編碼)的字符編碼,與讀取顯示的編碼(解碼)不一致產生的。
  •   需要用相同的字符編碼集進行  字符->二進制->字符  的轉換過程, 以避免亂碼問題的產生。

  思考題:Java中遍地的String, 是如何在內存中存儲的呢?使用何種編碼呢?

  相關解釋見這里 Java String編碼https://www.cnblogs.com/uncleguo/p/16076173.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM