字元編碼簡史,以及亂碼和他們的產地
這篇文章希望用講故事的方式,儘量避開一些技術上的眉眉角角,通俗地介紹字元編碼的簡單歷史,以及亂碼的成因和解法。
眾所周知,對於電腦的世界來說,所有的資訊都是 0 和 1,所以電腦其實最初是聽不懂人話的。 但,在電腦之前,在那更加野性而奔放的年代,其實「字元編碼(Character encoding)」的故事卻早得太多。
電報是人與人之間(用電線的)連結
在那個還是齒輪機械龐克的年代,「電報(Telegraph)」剛剛誕生,那時候沒有螢幕、沒有電腦、甚至連電話、無線電都還沒誕生。人們希望通過電線進行人與人之間的連結,即「通」和「不通」,可那時候沒有什麼自動編碼解碼的智能玩意兒,全靠「人工智慧」,用人手按開關和人耳聽電流聲,僅以此進行通訊。
而僅靠「通」和「斷」,聽起來是「嗶」和「靜」,用起來有點不太方便,所以我們就換一種方式,改成用「通電的長短」區分,再以「斷」加以間隔(這在通訊領域可以視為一種「調變」),再給每個字母一組「代碼」,例如「A」就是「嗶嗶—」、「B」就是「嗶—嗶嗶嗶」⋯⋯這就是「摩斯電碼(Morse code)」。
沒錯,摩斯電碼可以視為一種字元編碼!「字元編碼」也就是把文字轉換成另外一種表現形式的「協定」。在電機的世界,大家相互理解靠的不是同理心,而且共同遵守一個「協定(Protocol)」,用同一種方式交流,就能相互理解。
電傳打字機是機器與機器的對話
然而隨著技術的發展,人門發明出了「打字機(Typewriter)」,我們有了真正意義上的機械鍵盤⋯⋯以及把字「打」在紙上的技術。然後,就有人想到遠端打字,把兩台打字機連起來,我在這裡按鍵盤,再那裡把字打出來?於是,結合了電報和打字機技術的「電傳打字機(Teleprinter)」誕生了。鍵盤輸入轉換成電訊號,經過電線傳送,遠端的打字機印出字。機器和機器對話的時代來了!
古早的機械沒有人那麼矯情,直接閱讀電的通斷,每個「通」和「斷」我們表示為「1」或「0」,我們稱之為「位元(Bit)」。為了讓機器自動處理傳遞的文字,重新設計了一套字元編碼,例如「ITA2」,用 5 個位元為一組,對應了某個「動作」,例如:「印出某個字元並後移一格」,以及一些特殊動作,比如「換行」和「回車」,為了方便理解,這些特殊動作被概念抽象成一個字元,稱之「特殊字元」或「控制字元」。
另起一行需要多少個字元
在機械打字機上,機器有一個「滑台/車台(Carriage)」,上面夾著紙張;每打出一個字母,滑台就會往右移一格;打到一行末尾時,需要兩個動作才能繼續下一行:
- 「回車(Carriage Return,CR)」:滑台回到最左邊;
- 「換行(Line Feed,LF)」:滾筒轉動,使紙往上捲一行。
這兩個動作完全獨立,也可以分開執行。 由於「回車」的執行時間會比「換行」要長,所以通常是先回車再換行。 而由於它們實在是太常一起用了,有打字員就乾脆做了一個帽子,把這兩個按鍵連起來,按一下同時觸發,有一說這也是為什麼現代還有很多鍵盤佈局的回車鍵長得異常畸形的原因。
而這兩個特殊字元「CRLF」代表 實際上換行 的傳統延續至今,現代的一些網路協定和 Windows 都是如此。 又有人覺得為了傳統多寫一個字根本就是在浪費生命和頻寬和存儲空間,所以只保留其中一個作為實際上的換行,例如早年 Mac 僅用「CR」,以及現代 Mac 和 Linux 僅用 「LF」。
牙牙學語的電腦
話說回來,那時候的電腦還是幾個衣櫃大的機器,更沒有便利的螢幕。那時候的電腦還靠著音樂盒一樣的打孔紙條進行輸入。之後電傳打字機被連到了電腦上,電腦和用戶對話的時代來了!這時候為了便利電腦和打字機的通訊,7 位元的「ASCII」編碼被制定了出來,涵蓋了大小寫英文字母、數字和基本符號,成為了現代字元編碼的基石。現代幾乎所有主流字元編碼都基於它。
同樣的,字元編碼規定了一個被標記為文字的數字該如何解讀,例如,0x41 就是字母 A,實際存在硬碟上就是 01000001。「字型(Fonts)」就是按照這套規範,畫出這個字元的樣子。
而只有拉丁字母顯然是不夠用的,先撇開漢字文化圈不談,通過擴展 ASCII 變成 8 個位元,就多出 128 個位置,讓用字母的音位文字語言可以塞自己的字母。被稱為「擴展 ASCII 編碼」。終於,一個字元用八個位元等於一個「位元組(Byte)」的概念被確立,這也是為什麼 C 語言裡面一個位元組叫做 char 的原因。
而漢字卻是最麻煩的,很明顯只多出來的 128 個位置是塞不下那成千上萬個常用漢字的,所以我們會需要用到兩個甚至三個位元組來表示一個漢字。
自說自話又被各自解讀
ASCII 正如其名是為了美國和美式英語設計的,而電腦傳播到了全世界,ASCII 也有自然需要融合各地的口味。例如日本當時出於常用的理由,相容 ASCII 的 JIS X 0201 把 0x5C 對應的「反斜線 \」改定義為「日圓符號 ¥」,這也是為什麼在 Windows 日本版的電腦上路徑(看上去)是用 ¥ 分割的,這讓外國人看得不可思議,但實際上在底層是一致的。
那時候,全世界都在設計自己的字元編碼,甚至每個公司都有自己的編碼,被叫做「萬碼奔騰」的年代,光是台灣就有好幾種繁體字的編碼方式遍地開花。當時席捲全球的 IBM PC DOS 為了能夠席捲全球,將這些字元編碼統整成冊,稱之「代碼頁(Code Pages)」。例如英文的 CP437、繁體中文的 Big-5 叫做 CP950、日文 Shift-JIS 叫做 CP932,後來被 Windows 沿用。而這種「一個代碼、各自表述」的編碼方案,稱之「ANSI 編碼」(雖然和那個 ANSI 基本上八竿子打不著)。
這種自說自話的編碼系統有個很明顯的問題,就是難以跟外國人搭話:例如,用拉丁字母的英文的編碼就沒法顯示用西里爾字母的俄文,繁體中文的編碼就難以顯示有假名的日文。一旦跨國交流就會語言不通非常尷尬,例如用 Big-5 編碼的系統打開 Shift-JIS 編碼的檔案,就會因為「不同的解讀」就會變成「亂碼(Mojibake)」。解決方式就是手動指定代碼頁。
萬碼歸一萬國碼
為了更方便讓各國進行文化輸出,以及解決那些舊時代的限制,電腦科學家們想到,若是只用一套字元編碼,就能包天包海的能夠包下世界上所有的字元,這就是「萬國碼(Unicode)」。
Unicode 為世界上所有的字元分配一個代碼點(Code Point):包括了常見的、不常見的、從古至今活著的、失傳的、甚至虛構的大大小小林林總總各種各樣語言的字母和符號,也囊括了成千上萬個漢字即「中日韓統一表意字元」,甚至還有撲克、西洋棋之類的符號,還有表情符號 Emoji 也是其中的一部分。
如果我們直接把這個字元對應的四位元組數字直接記錄下來,就是「UTF-32」,但電腦世界最常用的拉丁字母只在最前面的一個位元組中,這麼記錄很顯然浪費了三倍的空間。於是天才的計算機科學家們通過「可變長度編碼」的方式,讓靠前的字元比如英文用的拉丁字母僅用一個或兩個位元組表示,這就是「UTF-8」和「UTF-16」。最常用的正是 UTF-8,Windows 上對應的代碼頁是 CP65001。
終於,字元編碼變成了「書同文」的大同世界。
前向相容是歷史遺留問題
其實 Windows 很早就用上了 Unicode ,自從比 XP 還早的 Windows 2000 的 NT 核心以來,Windows 就用上了 UTF-16LE(小端序)作為作業系統和檔案系統的唯一指定的字元編碼。可是事情並不會那麼簡單:老的程式和檔案格式尚未支援 Unicode。
為了和平友善地支援向前相容老舊程式,Windows 至今保留了舊時代的介面和預設代碼頁的設定,所以舊 ANSI 時代的亂碼問題我們也仍然還會遇到。例如說資料交換、壓縮檔、玩日文遊戲等場景。這正是亂碼和他們的產地。而且因為我們是在解決歷史問題,事情可能會變得更加的錯綜複雜。
舉一個實在的例子,古早的 ZIP 壓縮檔的設計,沒有考慮到這種跨語系的場景,其中被壓縮檔案的檔名是 ANSI 編碼的,至於具體是哪個編碼,它不知道,因為根本沒記下來。(後來才新增 Unicode 支援。)
假如我們要在英文(CP437)的系統上,打開一個在日文(Shift-JIS)系統上壓縮的 ZIP 檔。那麼,被壓縮檔案的檔名是以 Shift-JIS 編碼記錄在壓縮檔內。然後這個檔案在繁體中文的作業系統上,同樣的數字被以 CP437 的方式解讀,就會變成亂碼。 然後我們直接解壓縮這個檔名亂碼的檔案,它們就會被單純明快地「字面翻譯」成 UTF-16LE 存放在硬碟上。
=
=
=
: =
=
=
=
Original : はじめにお読みください.txt
Bytes in Archive: 82CD82B682DF82C982A893C782DD82AD82BE82B382A22E747874
CP437 View : é═é╢é▀é╔é¿ô╟é▌é¡é╛é│éó.txt
Stored in Drive : E9005025E9006225E9008025E9005425E900BF00F4005F25E9008C25E900A100E9005B25E9000225E900F3002E00740078007400
Recovered : はじめにお読みください.txt
這也是如果你用 7Z 或新版 ZIP 等支援 Unicode 的壓縮軟體再把這些亂碼名字檔打包起來,就算是還給日文系統,他們看起來仍然是亂碼。更可怕的是,如果又被舊時代的代碼頁再次轉換⋯⋯
那麼我們該如何修復呢,若是理想狀況,原本的 Shift-JIS 的字元對應到 CP437 中的字元可以顯示,我們就有辦法原路返回:UTF-16LE 轉 CP437,再用 Shift-JIS 解讀。但若是出現了無法對應的文字,例如 Shift-JIS 轉 Big-5 大多是沒字的,在 Unicode 轉換時,就會被替換成 � 菱形問號「替代字元(Replacement Character)」,這時候因為產生了資訊損失,就無法直接恢復了。當然最有效的方式,當然就是一開始就「正確解讀」原始的那個壓縮檔。
字元編碼的歷史就是人與機器相互理解的過程,可是就和人與人之間一樣,想要相互理解是很困難的,在沒法理解的時候,那就只能和解。