前言:
編碼一直是個有趣和容易讓人似懂非懂的議題,我們多少總有碰過亂碼的檔案,或是需要在不同編碼中轉或語系的需求;也許只需要去 Google 一下方法,或是藉由 Iconv
來做轉碼,但是對於亂碼的問題,或是編碼的含義依然一半解,又或著過了一陣子之後又得重新再 Google 找解法。與其不斷地盲目亂找治標不治本的答案或解法,倒不如真正有系統地去探索問題背後的緣由或脈絡,真正地去解決問題。
何謂編碼 (Encode)?
雖然我們無論是在寫程式或是在處理文件都是輸入英文、甚至中文、各種標點符號等等,但實際上電腦是無法認得這些符號的,所以勢必會將這些文字或符號轉化成數字,而且是電腦認得懂得數字格式。
ruby 版本
1 | 'Ruby is awesome'.bytes |
python 版本
1 | list('Ruby is awesome'.encode('utf-8')) |
我們得到一堆數字,而這些數字就是 Ruby is awesome
的編碼所得到的數字,也就是說:
編碼就是定義了字串如何對應到數字間的關係
而對應不同的編碼方式也就會得到不同的編碼
1 | # 預設是 utf-8 |
故,即使是相同的文字藉由不同的編碼方式就會得到不同的數字結果,也就是說當如果當如果得到與作業系統預設不同編碼格式的檔案,就是必得做一層編碼切換才可以取得正確地結果。
錯誤的編碼
以上為例,如果硬拿 big5 的編碼格式直接去取得對照 utf-8 的文字,因兩者的編碼格式不同,不同數字參照的文字當然也就不同,所以會取得錯誤的結果,甚至是得到無效的結果,也就是所謂的亂碼;常見的亂碼可能:
- 用與原始檔案不同的編碼去解碼,而得到錯誤的結果(拿明朝的劍去斬清朝的官)
- 承接 1 的結果,有可能會是可以解碼但結果無意義、或著是連解碼都失敗,因為有無效字元
- 相同語系的編碼,但其中一方的字集較少,轉換的時候會出現遺失(例如:『裏』在 utf-8 有收錄,而 big5 沒有)
- 檔案本身記錄的編碼格式是錯誤的,一開始就被騙了
其中 3 跟 4 都是屬於比較難處理的部份, 3 可能得自己造字,4 的話只能使用經驗法則去抽絲剝繭外加通靈了
編碼的演化
因為電腦最早是由美國人發明的,所以最早支援的也就是只有英文字母、數字、幾個常見的標點符號,例如:
ASCII (American Standard Code for Information Interchange)
1968 第一次發表,最早使用一個字節,即 1 byte = 8 bits,然後最前面的
bit 為 0 ,因此僅使用後面 7 個 bit 表示 128 種字元(含英文大小寫、數字、基本的符號),而在之後又有在經過一些拓展
但隨著電腦的普及,以及越來越多國家有自己的文字或符號想做表示,所以 ASCII 自然無法勝任,而各國也開始發展自己的字符編碼系統,以台灣為例, Big5
就是支援最早支援繁體中文的主流,收錄了 13xxx 個繁體中文字,而當然日本、韓國、中國、世界各地都有建置自己的符合自己需求的編碼系統,因此編碼的種類已經逐繁不及備載
,而且也衍生新的議題:
不同編碼轉換的需求
例如,如果我得作業系統內建只有支援 Big5
那如果我有一個檔案是 SHIFT-JIS
(日文編碼),我的作業系統將無法正確讀取此檔案內容,所以就得去再下載該編碼來顯示,或是去尋找能同時支援中、日語系的編碼並且將之轉化;但如果我下次想開啟韓文或是法文的編碼檔案,那我就得再尋找同時支援這幾種語系的編碼。
於是 Unicode 就誕生了
Unicode
Unicode 就是萬國碼,簡單的說就是一個包含目前所有常見語系編碼、與符號的超大字集表(目前有收錄一百多萬個;理想而言,大家都使用這個超大的字符集,應該就不會有轉換編碼的需求,但又出現另一個難題,既然有至少一百多萬個字元、符號(而且未來可能還會更多),那就需要更多個字節 (byte) 去儲存他,這種情況對中文可能還好(因為中文本來就需要 2 或更多個字節去儲存),但對英文就顯得非常浪費了,因為英文本來就只需要用一個字節去儲存,但使用了 Unicode 後可能被迫使用 4 個字節去儲存它。
因此儲存 Unicode 顯然是一種空間浪費。
UTF-8
UTF-8 就是為了解決這個問題而誕生的,UTF-8 就是一種 Unicode 的 表現方式
,而且他的字節數是可變動的,例如當碰到數字或英文時它會使用一個字節去儲存,如果碰到中文或其他的符號,它會使用多個字節去儲存,從而達到支援多語系轉換,又不浪費儲存空間的特性,而且另一個特性是,因為使用一個字節來儲存英文與數字,所以當內文只含數字與英文時,又可以與 ASCII 完美契合。
UTF-8 如何達到這種神奇的方式呢?它如何讓系統知道哪幾個字節要一起使用,哪幾個不用呢?這就在於他的轉換原理:
- 如果是英文或數字,那僅佔用一個字節,而此時此字節的二進位的表示與 ASCII 一致
- 當如果是需要多個字節表示時,它有特定的規則去組合
先觀察第一個字節開頭有幾個 1
就表示要幾個字節
當做一個字看,而後面會接一個 0 後再接續剩下的編碼加上之後每個字節去掉開頭是10
的部份就是最後的 Unicode 編碼
‘1110xxxx 10xxxxxx 10xxxxxx’ => 表示三個字節
1 | "陳".bytes.map { |c| '%08d' % c.to_s(2) } |
因此,我們得知 UTF-8 是一種 Unicode 的 表現格式
,但又較能 節省空間
,所以也目前最被廣泛使用與支援的檔案格式。
當不知道要使用什麼編碼的話就用 UTF-8 吧
Unicode 與編碼的關係
目前大部分的系統都已內建與支援 Unicode ,當檔案已讀到到記憶體時,便是以 Unicode 的形式存在,而當需要寫入到檔案、DB,或是要藉由網路傳輸時,會再由 Unicode 轉換成編碼格式後的數據寫入或傳送(省空間)。
除此之外,不同語系編碼的轉換也有賴於 Unicode 的存在;我們已經知道 Unicode 已經涵蓋大多主流的語系,所以我們也可以藉由 Unicode 來轉換語系;而此時我們需借用 代碼頁 (codepage)
來標示不同編碼與 Unicode 的映射關係,並進行轉換。而我們最常使用的工具就是 Iconv
來幫助我們做到這一塊,而它的背後也是需要借助 Unicode 來完成的。(詳盡的轉換細節我並未深入,所以在此只能講個粗略的原理)。
Ruby Encode
花了很長的篇幅粗略交代與介紹編碼的由來,這樣大家對編碼的使用與操作也比較有些概念與認識,也比較容易切入之後的介紹。
在 Ruby 裡我們可以直接使用 encoding
來查看目前的編碼
1 | str = 'Ruby 就是潮!' |
並且使用 encode
來切換編碼
1 | str.encode('big5') |
不是所有的語系都可以隨意轉換
1 | str.encode('windows-1252') |
但可以利用 invalid
, undef
來跳脫無效或無法定義的字元
1 | str.encode("Windows-1252", invalid: :replace, undef: :replace) |
不過此舉雖然可以成功 encode ,但基本上會失去不少資訊,
所以幫助也不是很大
還有個方法 force_encoding
可以幫助我們強行把編碼轉換並且 顯示
出來
1 | str.force_encoding('big5') |
修正錯誤編碼
這大概是最重要又最難的一個章節,如 Justin Weiss
的建議,當碰到錯誤編碼的時候,可用三個步驟來修正問題:
1. 找出現在字串的 真正編碼
雖然我們可以透過 encoding
來找出編碼,但實際上有可能依然是錯誤的
1 | str = '100073511445||¥_¥x¤¤Àç·~©Ò|20171103065705|2497319201|00006|°t°e¤¤|'.encoding' |
UTF-8 !!!? 四眼田雞你唬我啊,因為 伴隨著奇怪的符號,我們可以猜測應該不是 UTF-8
,接下來就需要靠一些經驗法則外加通靈來尋找了。
由於一些老舊軟體綁定設定到某些特定的編碼,所以我們可以反向猜測該軟體或系統的編碼來做推敲,例如還是很多公司行號或公家機關使用 Windows 的系統,又如果是早期像是 XP 或是更早的 98 等等,可能就會有比較頑固的老編碼問題存在。
Did someone paste it in from Word? It could be Windows-1252. Did it come from a file or did you pull it from an older website? It might be ISO-8859-1.
如果還是不行,只能 從特殊字元去反搜
哪些編碼會使用到,再去做塞選,此時可以搭配一些線上工具 傳送門 幫助做人工驗證,而因為我知道這個編碼預期會是以中文的模式顯示,所以在交叉試驗後我得知:
此字串以
Windows-1252
做 encode,以Big5
做 deconde 時可以正確呈現
2. 決定要讓哪種編碼方式來使用
為了一勞永逸、為他人著想,以及方便日後在不同的作業系統或是語系做編輯,都推薦使用 UTF-8
就對了。(當然,如果你有很特定的需求也可以使用你想用的)
3. 重複步驟 1
與步驟 2
直到成功
encode
的第二個參數可以是你想 從哪個編碼過來 / source
,所以 encode(target, source)
一般而言就是你要的結果。
1 | str.encode('utf-8', 'big5') |
Oops 還是不行,因為我這個例子比較怪異,記得我步驟 1
以提到,要正確呈現必須是 encode with Windows-1252, and decode with Big5
,所以必須經由兩次 encode 才可以成功,修正結果如下:
1 | str.encode('windows-1252','utf-8').encode('utf-8', 'big5') |
其實就是要先讓 假的 UTF-8
變成 Windows-1252
,再由 Big5
讓他可以成功 decode
,最後再把這個正確的結果 encode 為 UTF-8
就大功告成了。
結論
編碼的學問很大,而且當碰到錯問題的時候也真的是可以搞死人,所以對編碼有著正確地認識與了解,可以先幫助我們有個脈絡,再藉由其他線上工具以及經驗法則,我們將可以解決絕大部分的問題,並且把這個知識系統化、傳承下去。
參考
3 Steps to Fix Encoding Problems in Ruby