Learning Objectives
- 說明字元集(每個字元的編號)與編碼(編號怎麼存成位元組)的差別
- 認識 ASCII、Big5 與 Unicode,看懂 U+XXXX 碼位的寫法
- 依照 UTF-8 的規則,判斷一個字元要用幾個位元組、每個位元組的前綴代表什麼
- 解釋亂碼的成因:寫入與讀取用了不同的編碼,並知道怎麼修正
亂碼從哪裡來
你可能遇過這些畫面:同學傳來的 CSV 檔打開變成 䏿–‡、舊網頁的中文變成「嚙踝蕭」、字幕檔的中文全部變成問號或「�」。
檔案本身通常沒有壞。電腦裡沒有「字」,只有位元組;每個字元都要先換成數字,再把數字存成位元組。 存的時候用哪一套規則、讀的時候又用哪一套規則,只要兩邊不一樣,就會出現亂碼。
這一課要回答:
- 字元怎麼變成數字?ASCII、Big5、Unicode 各是什麼?
- Unicode 的數字怎麼存成位元組?UTF-8 的規則是什麼?
- 亂碼是怎麼產生的?要怎麼修好?
先分清楚兩個名詞:
| 名詞 | 意思 | 例子 |
|---|---|---|
| 字元集(character set) | 有哪些字元,每個字元的編號是多少 | Unicode 規定「中」的編號是 U+4E2D |
| 編碼(encoding) | 編號怎麼存成一串位元組 | UTF-8 把 U+4E2D 存成 E4 B8 AD 三個位元組 |
ASCII、Big5 這類早期標準把兩件事合在一起定義;Unicode 則把它們分開。
ASCII 與各地的擴充
ASCII(American Standard Code for Information Interchange)第一版在 1963 年發布,用 7 個位元表示 128 個字元(小寫字母在 1967 年的修訂版才加入):
| 編號(十進位) | 十六進位 | 內容 |
|---|---|---|
| 0~31、127 | 00~1F、7F | 控制字元,例如換行(10)、Tab(9) |
| 32 | 20 | 空白 |
| 48~57 | 30~39 | 數字 0~9 |
| 65~90 | 41~5A | 大寫 A~Z |
| 97~122 | 61~7A | 小寫 a~z |
大寫與小寫剛好差 32,也就是只差一個位元(A = 0100 0001、a = 0110 0001)。
一個位元組有 8 個位元,ASCII 只用了一半。128~255 這一半,各地拿來放自己的字:西歐的 Windows-1252 放了 é、ñ、ü 這類字母;中文字有上萬個,一個位元組放不下,就用兩個位元組表示一個字。
台灣常用的是 Big5:1984 年由資訊工業策進會(資策會)與五家電腦廠商共同制定,收錄常用字與次常用字共一萬三千多個漢字,例如「中」是 A4 A4。中國用 GB2312、日本用 Shift_JIS,各自規定不同。
問題就出在這裡:同一組位元組,在不同的編碼裡代表不同的字。檔案裡沒有寫明自己用哪一種編碼,讀的程式只能猜,猜錯就是亂碼。
Unicode:全世界的字元一個編號
Unicode 想解決的是「各地各自為政」:給世界上每一種文字的每一個字元一個唯一的編號,稱為碼位(code point),寫成 U+ 加上十六進位數字。第一個版本在 1991 年發布,之後持續加入新的文字與表情符號。
| 字元 | 碼位 |
|---|---|
A |
U+0041(和 ASCII 的 65 一樣) |
é |
U+00E9 |
中 |
U+4E2D |
😀 |
U+1F600 |
碼位的範圍是 U+0000 到 U+10FFFF,大約一百一十萬個位置。
Unicode 只規定編號;編號要怎麼存成位元組,另外有幾種編碼方式:
| 編碼 | 每個字元的長度 | 常見用途 |
|---|---|---|
| UTF-8 | 1~4 個位元組 | 網頁、檔案、大多數程式語言的原始碼 |
| UTF-16 | 2 或 4 個位元組 | Windows 內部、Java 與 JavaScript 的字串 |
| UTF-32 | 固定 4 個位元組 | 程式內部處理,較少用來存檔 |
UTF-8 的規則
UTF-8 依碼位大小決定用幾個位元組,第一個位元組的開頭(前綴)告訴你這個字元一共有幾個位元組:
| 碼位範圍 | 位元組數 | 位元樣式(x 填入碼位的位元) | 例子 |
|---|---|---|---|
| U+0000~U+007F | 1 | 0xxxxxxx |
A → 41 |
| U+0080~U+07FF | 2 | 110xxxxx 10xxxxxx |
é → C3 A9 |
| U+0800~U+FFFF | 3 | 1110xxxx 10xxxxxx 10xxxxxx |
中 → E4 B8 AD |
| U+10000~U+10FFFF | 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
😀 → F0 9F 98 80 |
這套設計有幾個好處:
- 和 ASCII 相容:英文字母、數字只用 1 個位元組,而且和 ASCII 完全相同,舊的英文檔案本來就是合法的 UTF-8。
- 不容易錯位:開頭是
10的位元組一定是「接續位元組」,從任何地方開始讀,都能找到下一個字元的開頭。 - 代價是常用中文字要 3 個位元組,比 Big5 的 2 個多。
UTF-8 是 Ken Thompson 與 Rob Pike 在 1992 年設計的,最早用在貝爾實驗室的 Plan 9 作業系統。
手動把「中」編成 UTF-8
「中」的碼位是 U+4E2D,在 U+0800~U+FFFF 之間,要用 3 個位元組:1110xxxx 10xxxxxx 10xxxxxx。程式用位元運算把碼位的 16 個位元切成 4、6、6 三段,分別填進三個位元組。>> 是往右移(丟掉右邊的位元),& 是只留下某幾個位元,| 是把兩組位元合在一起。最後和 Python 內建的 encode() 比較。
想一想:碼位與位元組
A 的 ASCII 編號是 65,C 的編號是多少?
一個 UTF-8 字元的第一個位元組是 11110xxx,這個字元一共用了幾個位元組?
下面的程式會印出什麼?
print(len('台灣'), len('台灣'.encode('utf-8')))
亂碼的成因與修正
亂碼 =「寫入時用的編碼」≠「讀取時用的編碼」。幾種常見的樣子:
| 看到的 | 通常的原因 |
|---|---|
䏿–‡、café |
UTF-8 的檔案被當成西歐編碼(Windows-1252)讀:一個字的 2~3 個位元組被拆成好幾個西歐字母 |
| 不相干的中文字,夾雜「�」 | UTF-8 的檔案被當成 Big5 讀,或反過來 |
| 「嚙踝蕭」 | 文字先被換成替代字元「�」(UTF-8 是 EF BF BD),這些位元組再被當成 Big5 讀 |
? |
存檔時用的編碼根本沒有這個字(例如用 Big5 存表情符號),存檔當下就換成問號,資料已經遺失 |
前三種只是讀錯,位元組都還在,換成正確的編碼重新開啟就好;最後一種是存錯,資料已經不見了。
避免亂碼的作法:
- 存檔一律用 UTF-8;程式讀寫檔案時明確指定,例如 Python 的
open('data.csv', encoding='utf-8')。 - 網頁在
<head>寫<meta charset="utf-8">,告訴瀏覽器用哪一種編碼。 - 文字編輯器多半有「以其他編碼重新開啟」的功能,可以用來救回讀錯的檔案。
- 有些試算表軟體開啟 UTF-8 的 CSV 檔時會猜錯編碼,可以改用「匯入」功能並指定 UTF-8。
在 Python 裡編碼與解碼
Python 的字串(str)存的是字元;encode() 把字串換成位元組(bytes),decode() 把位元組換回字串。注意 len() 算字串與算位元組的結果不同,以及最後用錯的編碼 cp1252(Windows-1252)解碼時,印出了熟悉的亂碼。
想一想:亂碼怎麼辦
同學用 UTF-8 存的 CSV 檔,你的軟體用 Big5 開啟,中文全部變成亂碼。這個檔案怎麼了?
下面的程式會印出什麼?
print(len('😀'), len('😀'.encode('utf-8')))
一份文件裡同時有中文、日文假名與表情符號,存檔時應該選哪一種編碼才不會遺失任何字?
重點整理
- 字元集規定每個字元的編號,編碼規定編號怎麼存成位元組;Unicode 是字元集,UTF-8、UTF-16 是它的編碼方式。
- ASCII 用 7 個位元表示 128 個英文字元;Big5 用 2 個位元組表示一個中文字;各地標準互不相容。
- Unicode 碼位寫成 U+XXXX,範圍是 U+0000~U+10FFFF;「中」是 U+4E2D。
- UTF-8 用 1~4 個位元組,第一個位元組的前綴(0、110、1110、11110)說明長度,接續位元組以 10 開頭;英文和 ASCII 相同,常用中文字 3 個位元組,表情符號 4 個。
- 亂碼是寫入與讀取用了不同的編碼;位元組還在就能用正確的編碼救回,存檔時就變成「?」的字已經遺失。存檔與讀檔一律明確使用 UTF-8。
下一課:文字之外,照片和音樂又是怎麼變成位元的?為什麼一張照片未壓縮要好幾 MB,壓縮後卻小很多?