亂碼問題是不但是新手程序員之痛,也常常讓許多資深 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)的樣子大致如下:
注:我們習(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è) GBK 編碼的文本文件會(huì)出現(xiàn)亂碼:
- 用 UTF-8 編碼打開一個(gè) UTF-8 編碼的文本文件不會(huì)亂碼:
綜上哑舒,亂碼的根源就是編碼與解碼用的不是同一套規(guī)則。 但不管文件是否亂碼幻馁,它里面保存的二進(jìn)制數(shù)據(jù)總是不變的洗鸵。通常情況下,亂碼并不是文件本身有問題宣赔,而是打開方式(解碼方式)不正確预麸。
三瞪浸、編程中出現(xiàn)亂碼的原因與類型
我們?cè)谌粘J褂梦谋揪庉嬈魅褰DE、命令行等編寫和執(zhí)行程序的過程中对蒲,常常會(huì)遇到亂碼現(xiàn)象钩蚊,而出現(xiàn)亂碼的原因是多種多樣的。這里試圖從根源上理解亂碼蹈矮,并將其歸類砰逻。
一般,我們編寫和執(zhí)行程序的流程如下:
- 編寫代碼并保存泛鸟;
- 調(diào)用編譯器編譯代碼蝠咆,并執(zhí)行程序;
- 查看輸出結(jié)果。
在這短短的三步操作中刚操,隱含著兩次編碼和解碼過程闸翅,也就是下圖中的過程 1 和過程 2:
在過程 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ā)生亂碼级乍,如下:
從這里我們也可以看出舌劳,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í)行腳本撵割,再次得到亂碼,如下:
注意:這里雖然也是亂碼辙芍,但與類型 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í)行腳本,得到亂碼跳仿,如下:
注意:這里的亂碼與類型 1 相同诡渴,都是用 GBK 編碼解釋 UTF-8 字符串造成的。
類型 3:過程 1 與過程 2 同時(shí)亂碼菲语。
亂碼是可以疊加的妄辩,即亂碼后的字符串可以再次被亂碼,得到的亂碼與疊加前的亂碼均不同山上。
我們讓 Python.sublime-build
文件保持上一步的狀態(tài)眼耀,然后在 Python 腳本的開頭重新加上一行 # -*- coding: gbk -*-
。執(zhí)行腳本佩憾,會(huì)得到前兩種完全不同的亂碼哮伟,如下:
以上就是編程中出現(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)行控制變量式的排除匪煌,如果能夠解決你的問題,那便是本文最大的成功蕉饼。