編碼與亂碼——追根究底

亂碼問題是不但是新手程序員之痛,也常常讓許多資深 coder 束手無策塑娇。最近在社區(qū)接連收到關(guān)于亂碼問題的求助再扭,五花八門靶病,我覺得是時(shí)候深入討論一下這個(gè)問題了。本文試圖讓讀者深入理解編碼的概念以及亂碼的產(chǎn)生的原理兔跌,以至于今后再遇到亂碼問題沃于,能夠獨(dú)立分析菱涤、解決它呀。
由于 Sublime Text 是筆者最青睞的編輯器螺男,因此文中的所有截圖和實(shí)驗(yàn)均以 Sublime Text 為例,其他編輯器或 IDE 在原理上是類似的纵穿。

一下隧、什么是編碼?

什么是編碼政恍?這要從「文件」的概念說起。根據(jù)呈現(xiàn)形式达传,文件可分為兩種類型:「文本文件」和「二進(jìn)制文件」篙耗。

二者的區(qū)別非常明顯,文本文件中保存的是各種字符宪赶,包括英文字母如 abc宗弯、漢字如 你好、日文如 こんにちは 等搂妻;而二進(jìn)制文件中保存的則是 0101 等二進(jìn)制數(shù)值蒙保。如果你用 Sublime Text 分別打開文本文件和二進(jìn)制文件,那么它們呈現(xiàn)的樣子大致如下:

文本文件與二進(jìn)制文件

注:我們習(xí)慣采用十六進(jìn)制的方式簡(jiǎn)化二進(jìn)制數(shù)據(jù)的顯示欲主,這樣對(duì)人類用戶稍微友好一些邓厕,避免了過長的 0-1 串使得人們眼花繚亂。

為什么會(huì)產(chǎn)生這兩種類型的文件呢扁瓢?一個(gè)非常直接的原因是详恼,文本文件主要是給人類用戶看的,例如我們常使用的 txt引几、markdown 文件昧互,各種代碼文件如 .cpp.java伟桅、.py敞掘、.js 等,以及各種配置文件如 .ini楣铁、.json 等玖雁;而二進(jìn)制文件則是給操作系統(tǒng)或應(yīng)用程序看的,如 .exe 交給 Windows 系統(tǒng)執(zhí)行盖腕、Word 文檔交給 Office Word 軟件打開茄菊、.class 文件交給 java 虛擬機(jī)執(zhí)行疯潭,許多應(yīng)用程序都會(huì)設(shè)計(jì)自己專用的二進(jìn)制文件格式。

盡管我們把文件分為文本文件和二進(jìn)制文件兩種類型面殖,但從計(jì)算機(jī)硬件層面上來看竖哩,它只能存儲(chǔ) 0101 這樣的二進(jìn)制數(shù)據(jù),不可能直接存儲(chǔ) abc 這樣的字符脊僚。那么該如何解釋文本文件的存在呢相叁?

事實(shí)上,從存儲(chǔ)方式上來看辽幌,文件確實(shí)只有一種類型增淹,那就是二進(jìn)制文件。至于文本文件乌企,它只是二進(jìn)制文件的一種特殊情況虑润。在計(jì)算機(jī)最初發(fā)明的時(shí)候,確實(shí)只有二進(jìn)制文件加酵,那時(shí)的人們通過「打孔的紙帶」作為存儲(chǔ)程序的載體拳喻,而紙帶上小孔的有無就代表二進(jìn)制的 1 和 0。那時(shí)候的計(jì)算機(jī)根本沒有字符的概念猪腕,更不要說文本文件冗澈。

后來,人們?yōu)榱朔奖憔椭贫艘惶滓?guī)則陋葡,規(guī)定二進(jìn)制數(shù)值 01100001 代表字符 a亚亲、01100010 代表字符 b、……腐缤、01111010 代表字符 z捌归。于是,最早的編碼「ASCII 編碼」就產(chǎn)生了×朐粒現(xiàn)在陨溅,如果我在一個(gè)文件中寫入二進(jìn)制數(shù)據(jù) 011000010110001001100011,從表面上看绍在,它就是一個(gè)常規(guī)的二進(jìn)制文件门扇,沒有任何特殊之處,但如果我用 ASCII 編碼的規(guī)則去解釋它偿渡,就會(huì)看到一串字符 abc臼寄。這時(shí)候,我們就可以認(rèn)為這個(gè)文件是文本文件溜宽。

從上面的描述中吉拳,你應(yīng)該已經(jīng)發(fā)現(xiàn):

  • 所謂的「編碼」就是一種規(guī)則,它規(guī)定了二進(jìn)制數(shù)值與字符之間的映射關(guān)系适揉;
  • 所謂的「文本文件」就是一種二進(jìn)制文件留攒,只不過能用某種編碼解釋得通煤惩。

說回到 ASCII 編碼,它使用 8 個(gè)二進(jìn)制位——也就是 1 個(gè)字節(jié)來映射一個(gè)字符炼邀,這意味著它最多只能映射 2^8=256 個(gè)字符魄揉。256 個(gè)字符對(duì)于純英文來說已經(jīng)足夠了,但世界上的語言太多了拭宁,要囊括英文洛退、德文、法文杰标、中文兵怯、日文、韓文腔剂、阿拉伯文媒区、希伯來文等所有語言文字,至少需要十幾萬的字符量掸犬。隨著各種文字不斷被引入計(jì)算機(jī)袜漩,字符編碼的長度也不斷擴(kuò)張,從 1 個(gè)字節(jié)逐漸增加到 2 個(gè)登渣、3 個(gè)噪服、4 個(gè)字節(jié)毡泻。同時(shí)胜茧,各個(gè)組織、各個(gè)國家都在制定自己的編碼體系仇味,形成了錯(cuò)綜復(fù)雜的編碼“方言”呻顽。最終,到了 1994 年丹墨,人們終于制定出了一套統(tǒng)一的廊遍、無所不包的編碼——Unicode 編碼,成為編碼界的“世界語”贩挣,因此也被稱為萬國碼喉前。

Unicode 編碼使用 4 個(gè)字節(jié)來保存字符映射關(guān)系,因此共支持 2^(4*8)=4294967296 個(gè)字符王财,遠(yuǎn)遠(yuǎn)超出了地球上所有文字的總量卵迂。這徹底解決了字符數(shù)量不夠用的擔(dān)憂,但也帶來了存儲(chǔ)空間的浪費(fèi):即使僅僅保存一個(gè)簡(jiǎn)單的英文字母 a绒净,Unicode 編碼也需要 4 個(gè)字節(jié)见咒,但事實(shí)上只需要 1 個(gè)字節(jié)(ASCII 編碼)。如果一個(gè)文本文件中絕大部分字符都是英文字母挂疆,那么 Unicode 就浪費(fèi)了 75% 的存儲(chǔ)空間改览。鑒于上述問題下翎,人們又制定了一系列“改良版”的 Unicode 編碼,包括 UTF-8宝当、UTF-16视事、UTF-32 等,它們同樣能夠編碼所有已知的字符今妄,但占用更少的空間郑口。

以 UTF-8 為例,對(duì)于常見的英文字符盾鳞,它采用 1 個(gè)字節(jié)編碼犬性,常見的中文、日文等字符采用 2 個(gè)字節(jié)腾仅,不常見的中文字符等采用 3 到 4 個(gè)字節(jié)乒裆,對(duì)于極不常見的字符,它會(huì)采用 6 個(gè)字節(jié)進(jìn)行編碼推励。因此鹤耍,在通常情況下,UTF-8 編碼要比 Unicode 編碼節(jié)省超過一半的空間验辞。UTF-8 編碼無所不包稿黄、節(jié)省空間,且具有良好的跨平臺(tái)性跌造,因此推薦一切文本文件都使用 UTF-8 編碼杆怕。目前,主流的文本編輯器都把 UTF-8 作為默認(rèn)編碼方式壳贪。

最后解釋一下所謂的「ANSI 編碼」陵珍。ANSI 編碼常被稱為標(biāo)準(zhǔn)編碼,但它并不是指某種明確的編碼方式违施。為了更容易地理解 ANSI 編碼互纯,我們不妨把它與「官方語言」的概念做類比。正如中國的官方語言是漢語磕蒲,日本的官方語言是日語一樣留潦,中文 Windows 系統(tǒng)的 ANSI 編碼為 GBK 編碼,而日文 Windows 系統(tǒng)的 ANSI 編碼為 Shift_JIS 編碼辣往。正如「官方語言」不是某種語言兔院,「ANSI 編碼」也不是某種編碼,它是另一個(gè)維度的概念排吴,與國家和地區(qū)有關(guān)秆乳,不同國家和地區(qū)的 ANSI 編碼是不兼容的。可想而知屹堰,如果都采用 ANSI 編碼肛冶,那么不同國家的開發(fā)者在互相交換代碼時(shí)將非常糟糕。因此扯键,不推薦以 ANSI 作為 coding 編碼睦袖。

二、什么是亂碼荣刑?

什么是亂碼馅笙?用某種編碼方式去解讀一個(gè)文件,得到了無意義的字符厉亏,這就是亂碼董习。打個(gè)通俗的比方:我寫了一段英文,你非要把它當(dāng)作拼音來讀爱只,那么得到的解釋就是無意義的皿淋,就相當(dāng)于亂碼;反過來恬试,我寫了一段拼音窝趣,你非要用英語的語法去解釋它,也是解釋不通的训柴。

舉幾個(gè)實(shí)際的例子:

  • 用 UTF-8 編碼打開一個(gè)二進(jìn)制文件會(huì)出現(xiàn)亂碼:
用 UTF-8 編碼打開一個(gè)二進(jìn)制文件
  • 用 UTF-8 編碼打開一個(gè) GBK 編碼的文本文件會(huì)出現(xiàn)亂碼:
用 UTF-8 編碼打開一個(gè) GBK 編碼的文本文件
  • 用 UTF-8 編碼打開一個(gè) UTF-8 編碼的文本文件不會(huì)亂碼:
用 UTF-8 編碼打開一個(gè) UTF-8 編碼的文本文件

綜上哑舒,亂碼的根源就是編碼與解碼用的不是同一套規(guī)則。 但不管文件是否亂碼幻馁,它里面保存的二進(jìn)制數(shù)據(jù)總是不變的洗鸵。通常情況下,亂碼并不是文件本身有問題宣赔,而是打開方式(解碼方式)不正確预麸。

三瞪浸、編程中出現(xiàn)亂碼的原因與類型

我們?cè)谌粘J褂梦谋揪庉嬈魅褰DE、命令行等編寫和執(zhí)行程序的過程中对蒲,常常會(huì)遇到亂碼現(xiàn)象钩蚊,而出現(xiàn)亂碼的原因是多種多樣的。這里試圖從根源上理解亂碼蹈矮,并將其歸類砰逻。

一般,我們編寫和執(zhí)行程序的流程如下:

  1. 編寫代碼并保存泛鸟;
  2. 調(diào)用編譯器編譯代碼蝠咆,并執(zhí)行程序;
  3. 查看輸出結(jié)果。

在這短短的三步操作中刚操,隱含著兩次編碼和解碼過程闸翅,也就是下圖中的過程 1 和過程 2:

代碼編寫和執(zhí)行過程中的編碼和解碼

在過程 1 和過程 2 中,任意一個(gè)過程兩端的編碼方式都必須一致菊霜,否則就會(huì)出現(xiàn)亂碼坚冀。其中,對(duì)于「代碼文件的編碼」以及「展示器的編碼」鉴逞,我們可以在編輯器和控制臺(tái)中進(jìn)行設(shè)置记某。最不可控的是編譯器的輸入編碼和輸出編碼,常見編譯器/解釋器的默認(rèn)輸入輸出編碼如下表所示:

編譯器/解釋器 默認(rèn)輸入編碼 默認(rèn)輸出編碼 設(shè)置輸入編碼 設(shè)置輸出編碼
python UTF-8 ANSI # coding=xxx 環(huán)境變量 PYTHONIOENCODING
gcc/g++ UTF-8 UTF-8 未知 未知
javac ANSI ANSI -encoding 參數(shù) 未知
matlab ANSI ANSI 修改配置文件 未知

注:該結(jié)果是筆者在自己的 Windows 10 家庭中文版上測(cè)試得到的构捡,不同的平臺(tái)可能有差異液南。


接下來,我們將以 Sublime Text 執(zhí)行一段 Python 腳本為例來展示這 2 種亂碼勾徽,通過設(shè)置編譯器輸入編碼贺拣、輸出編碼、展示器編碼來探究亂碼產(chǎn)生的不同原因捂蕴。

這段 Python 腳本非常簡(jiǎn)單譬涡,只有一句話:print('你好'),以 UTF-8 編碼保存啥辨。正常執(zhí)行的結(jié)果如下:

正常無亂碼

從上上圖中不難看出涡匀,過程 1 和過程 2 均能導(dǎo)致亂碼,其組合可形成如下三種亂碼類型:

類型 1:過程 1 亂碼

我們?cè)?Python 腳本頭部添加一行 # -*- coding: gbk -*-溉知,即把 Python 解釋器的輸入編碼指定為 GBK陨瘩,但腳本的編碼保持 UTF-8 不變。執(zhí)行結(jié)果將發(fā)生亂碼级乍,如下:

亂碼類型 1

從這里我們也可以看出舌劳,Python 解釋器的默認(rèn)輸入編碼為 UTF-8。

類型 2:過程 2 亂碼玫荣。

這里又分為兩種情況甚淡,一是編譯器的輸出編碼錯(cuò)誤;二是展示器的輸入編碼錯(cuò)誤:

2-1. 編譯器輸出編碼不當(dāng)捅厂。

打開 Python.sublime-build 文件(可借助 PackageResourceViewer 插件)贯卦,其初始內(nèi)容如下:

{
    "shell_cmd": "python -u \"$file\"",
    "file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
    "selector": "source.python",
    "env": {"PYTHONIOENCODING": "utf-8"},
}

我們把末尾的行改為 "env": {"PYTHONIOENCODING": "gbk"},,即把 Python 解釋器的輸出編碼設(shè)為 UTF-8焙贷。執(zhí)行腳本撵割,再次得到亂碼,如下:

亂碼類型 2-1

注意:這里雖然也是亂碼辙芍,但與類型 1 不同啡彬。

2-2. 展示器輸入編碼不當(dāng)羹与。

我們首先撤銷對(duì) Python.sublime-build 的所有更改,然后在其末尾增加一行內(nèi)容 "encoding": "gbk",庶灿,即把 Sublime Text 控制臺(tái)的編碼設(shè)為 GBK注簿。此時(shí) Python.sublime-build 配置如下:

{
    "shell_cmd": "python -u \"$file\"",
    "file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
    "selector": "source.python",
    "env": {"PYTHONIOENCODING": "utf-8"},
    "encoding": "gbk",
}

執(zhí)行腳本,得到亂碼跳仿,如下:

亂碼類型 2-2

注意:這里的亂碼與類型 1 相同诡渴,都是用 GBK 編碼解釋 UTF-8 字符串造成的。

類型 3:過程 1 與過程 2 同時(shí)亂碼菲语。

亂碼是可以疊加的妄辩,即亂碼后的字符串可以再次被亂碼,得到的亂碼與疊加前的亂碼均不同山上。

我們讓 Python.sublime-build 文件保持上一步的狀態(tài)眼耀,然后在 Python 腳本的開頭重新加上一行 # -*- coding: gbk -*-。執(zhí)行腳本佩憾,會(huì)得到前兩種完全不同的亂碼哮伟,如下:

亂碼類型 3

以上就是編程中出現(xiàn)亂碼的 3 種典型情況。需要指出的是妄帘,以上采用 Sublime Text 的控制臺(tái)作為展示器楞黄,其編碼可以通過 Build System 中的 encoding 參數(shù)進(jìn)行設(shè)置。如果你直接使用命令行如 cmd抡驼、bash鬼廓、cmder 等來編譯和運(yùn)行程序,那就完全省去這些麻煩了致盟,命令行一般會(huì)自動(dòng)識(shí)別你的輸出編碼碎税,因此總能使用正確解碼方式,基本不會(huì)出現(xiàn)類型 2 亂碼馏锡,但無法避免類型 1 亂碼雷蹂。

希望本文對(duì)你有所啟發(fā),如果你在編程中遇到了亂碼杯道,不妨對(duì)下圖中的 2 個(gè)過程進(jìn)行控制變量式的排除匪煌,如果能夠解決你的問題,那便是本文最大的成功蕉饼。

代碼編寫和執(zhí)行過程中的編碼和解碼
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末虐杯,一起剝皮案震驚了整個(gè)濱河市玛歌,隨后出現(xiàn)的幾起案子昧港,更是在濱河造成了極大的恐慌,老刑警劉巖支子,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件创肥,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)叹侄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門巩搏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人趾代,你說我怎么就攤上這事贯底。” “怎么了撒强?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵禽捆,是天一觀的道長。 經(jīng)常有香客問我飘哨,道長胚想,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任芽隆,我火速辦了婚禮浊服,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘胚吁。我一直安慰自己牙躺,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布腕扶。 她就那樣靜靜地躺著述呐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蕉毯。 梳的紋絲不亂的頭發(fā)上乓搬,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音代虾,去河邊找鬼进肯。 笑死,一個(gè)胖子當(dāng)著我的面吹牛棉磨,可吹牛的內(nèi)容都是我干的江掩。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼乘瓤,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼环形!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起衙傀,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤抬吟,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后统抬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體火本,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡危队,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了钙畔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片茫陆。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖擎析,靈堂內(nèi)的尸體忽然破棺而出簿盅,到底是詐尸還是另有隱情,我是刑警寧澤揍魂,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布挪鹏,位于F島的核電站,受9級(jí)特大地震影響愉烙,放射性物質(zhì)發(fā)生泄漏讨盒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一步责、第九天 我趴在偏房一處隱蔽的房頂上張望返顺。 院中可真熱鬧,春花似錦蔓肯、人聲如沸遂鹊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽秉扑。三九已至,卻和暖如春调限,著一層夾襖步出監(jiān)牢的瞬間舟陆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工耻矮, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留秦躯,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓裆装,卻偏偏與公主長得像踱承,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子哨免,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 字符集和編碼簡(jiǎn)介 在編程中常尘セ睿可以見到各種字符集和編碼,包括ASCII,MBCS,Unicode等字符集琢唾。確切的說...
    蘭山小亭閱讀 8,453評(píng)論 0 13
  • 編碼規(guī)則 如果你已經(jīng)閱讀了JavaHipster 1中references提到的兩篇文章载荔,你應(yīng)該明白:從字符集到編...
    褲lue閱讀 1,518評(píng)論 2 1
  • 字符是用戶可以讀寫的最小單位身辨。計(jì)算機(jī)所能支持的字符組成的集合丐谋,就叫做字符集芍碧。字符集通常以二維表的形式存在煌珊。二維表的...
    劉惜有閱讀 8,070評(píng)論 2 14
  • 常言說人生就像一段旅行踪危,不在乎終點(diǎn)蔬浙,在乎的是途中的風(fēng)景,其實(shí)旅行更不在乎終點(diǎn)贞远,更在乎途中的人和事畴博,以及那些...
    三文一顆閱讀 287評(píng)論 0 0
  • 涌化的春雨 撐破想你的窗 層層漣漪蕩起 想你的回憶 不多不少又是雨...
    零度lzc閱讀 423評(píng)論 0 12