親手組出 UTF-8
把碼位的位元灌進 1〜4 個位元組的模板 — 以 U+3042 → E3 81 82 手算 UTF-8 的轉換,並確認 ASCII 相容的原因與 BOM。
讓「為什麼是 3 個位元組」變成用計算就能回答的問題
「UTF-8 的日文是 3 個位元組」— 就算在哪裡聽過,你能說明為什麼是 3、那 3 個位元組裡裝的是什麼嗎?本章的目標只有一個:由你自己親手推導出 U+3042「あ」變成 E3 81 82 的過程。只要推導過一次,UTF-8 就不再是背誦科目,而是「可以計算的東西」。
UTF-8 是可變長度的編碼。依碼位的大小,1 個字元以 1〜4 個位元組表示。
| 碼位範圍 | 位元組數 | 模板(把號碼的位元灌進 x) |
|---|---|---|
| U+0000〜U+007F | 1 | 0xxxxxxx |
| U+0080〜U+07FF | 2 | 110xxxxx 10xxxxxx |
| U+0800〜U+FFFF | 3 | 1110xxxx 10xxxxxx 10xxxxxx |
| U+10000〜U+10FFFF | 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
模板中黑色的部分(1110 或 10)是固定的「印記」。開頭位元組中 1 的個數報出該字元的位元組數,後續位元組一定以 10 開頭 — 光憑這條規則,就能從位元組序列的任何位置找到字元的分界。
手算 — 把 U+3042 變成 3 個位元組
日文字元「あ」的 U+3042 落在 U+0800〜U+FFFF 的範圍,所以使用 3 位元組的模板。步驟只有 2 步。
0011 0000 0100 0010。從右邊每 6 個位元切開,得到 0011 / 000001 / 000010(4 + 6 + 6 個位元)11100011 10000001 10000010 → 換成十六進位就是 E3 81 82驗算看看。E3 = 1110 0011 — 開頭有 3 個 1,所以是「3 位元組字元的開頭」。81 = 1000 0001、82 = 1000 0010 — 都以 10 開頭,是「後續位元組」。把印記剝掉、剩下的位元接起來,就是 0011 000001 000010 = 0x3042,回到原點。來回都只是位元的重新裝箱。
碼位的位元被灌進模板的樣子。上色的位元是「あ」號碼的內容,黑色部分是 UTF-8 固定的印記。
小題 3-1 — 灌進模板
請照正文的步驟,在紙上寫下二進位邊算邊確認。
Q1. 從頭讀取 UTF-8 的位元組序列時,要怎麼知道某個位元組是「幾位元組字元的開頭」?
UTF-8 的設計是由開頭位元組自己報出長度。開頭連續 1 的個數就是該字元的位元組數(110→2、1110→3、11110→4),後續位元組一律是 10xxxxxx。多虧這條規則,即使從序列中間開始讀,也能立刻找到字元的分界。它不是固定長度,而是 1〜4 位元組的可變長度。
Q2. 把 U+3042(二進位為 0011 0000 0100 0010)灌進 3 位元組模板 1110xxxx 10xxxxxx 10xxxxxx 的正確步驟是哪一個?
模板的 x 從前面數是 4 個、6 個、6 個。把碼位的位元列從右邊以 6、6 切開會得到 0011 / 000001 / 000010,填入後是 11100011 10000001 10000010 = E3 81 82。把十六進位的 30 42「原樣放進去」是 UTF-16 的思路;UTF-8 會重新排布位元,這是兩者的差異。
Q3. ASCII 字元在 UTF-8 中是 1 個位元組,漢字與日文平假名多數是 3 個位元組。字串「Hello世界」以 UTF-8 表示共是幾個位元組?
Hello 是 5 個 ASCII 字元 × 1 位元組 = 5 位元組;「世」(U+4E16)與「界」(U+754C)都落在 U+0800〜U+FFFF 的範圍,各 3 位元組共 6 位元組。合計 11 位元組。「字元數 = 位元組數」並不成立 — 請用具體的數字把這件事記進身體裡。在資料庫的欄位寬度設計等場合一定會用到。
為什麼 ASCII 相容「行得通」
請仔細看 1 位元組的模板 0xxxxxxx。U+0000〜U+007F,也就是 ASCII 的 128 個字元,會變成與 ASCII 時代完全相同的 1 個位元組。只有英數字的檔案,當作 ASCII 讀或當作 UTF-8 讀都一模一樣 — 幾乎不必更動既有的程式與協定就能過渡,正是拜這個設計所賜。
還有一個容易被忽略的要害。組成 2 個位元組以上字元的位元組,不管是開頭(以 11 開始)還是後續(以 10 開始),一定在 0x80 以上。也就是說,日文等多位元組字元的位元組序列中間,絕對不會出現與 /(0x2F)或 "(0x22)這類 ASCII 符號相同的值。以位元組為單位尋找分隔符的程式,在結構上就不可能誤把字元的中間切開。這份安心並非理所當然 — 下一章的 Shift_JIS 會讓我們深刻體會。
用模擬器確認
在下面的模擬器輸入任何字串,就能一次確認本章手算的轉換。首先請用初始值、日文的「こんにちは」,看看 5 個字元變成 15 個位元組,而且每個字元都以 E3 開頭。也推薦輸入自己的名字、表情符號、中英混合的字串。輸入只在瀏覽器內處理,不會送到伺服器。
模擬器的運作概要(文字版):在文字輸入欄輸入字串後,每個字元的碼位(U+XXXX)、UTF-8 位元組序列(十六進位)、UTF-16 碼元會以表格形式更新。同時顯示 UTF-8 總位元組數、UTF-16 碼元數、碼位數、肉眼所見的字元數(字素)這 4 個數值,最後顯示「把這串 UTF-8 位元組序列當成 Shift_JIS 誤讀時會是什麼樣子」(若你的瀏覽器不支援 Shift_JIS 解碼,該欄會顯示「お使いのブラウザでは表示できません」=你的瀏覽器無法顯示)。
BOM — 檔案開頭的小小「記號」
在模擬器的最後一欄,我們看到了 UTF-8 位元組序列被當成 Shift_JIS 誤讀後的模樣。「能不能在檔案開頭放個記號,讓讀取方猜得中編碼?」— 答案之一就是 BOM(Byte Order Mark,位元組順序記號):把碼位 U+FEFF 以 UTF-8 編碼成 EF BB BF 這 3 個位元組,放在檔案的開頭。
顧名思義,它本來是 UTF-16 用來標示位元組排列順序(高位在前還是低位在前)的機制。UTF-8 沒有位元組順序的問題,所以 BOM 純粹作為「這是 UTF-8」的可選記號使用。有它就多一條線索;但對沒有預期開頭 3 個位元組的程式來說,它是多餘的資料,也可能成為誤動作的種子。「BOM 不是必須的。但 Excel 等軟體會依靠 BOM 判定編碼」— 這場拔河在實務上的結論,將在第 6 章處理。
小題 3-2 — ASCII 相容與 BOM
確認 UTF-8 能成為世界標準的原因,以及檔案開頭的「記號」。
Q4. UTF-8 的「ASCII 相容」能安全成立的原因,正確的是哪一個?
0x00〜0x7F 與 ASCII 在位元層級完全相同,而組成 2 個位元組以上字元的位元組(開頭與後續)最高位元一定是 1,也就是 0x80 以上。所以只有英數字的檔案原樣就是有效的 UTF-8;反過來,日文等多位元組字元的中間也絕不會出現 0x2F(/)之類的 ASCII 值。這個設計正是它能與既有 ASCII 前提的程式共存的最大原因。第 4 章要看的 Shift_JIS,正是在這裡出了問題。
Q5. 日文的「こんにちは」(5 個平假名,每個都是 3 個位元組)以 UTF-8 表示共是幾個位元組?
3 位元組 × 5 個字元 = 15 位元組。E3 81 93(こ)E3 82 93(ん)E3 81 AB(に)E3 81 A1(ち)E3 81 AF(は)— 請注意每個字元都以 E3 開頭:平假名的碼位(U+3040 左右)彼此相近,所以開頭位元組會一致。這種「滿滿的 E3」會在第 7 章作為亂碼的指紋發揮作用。
Q6. 關於 UTF-8 的 BOM(Byte Order Mark),正確的說明是哪一個?
BOM 的實體是把 U+FEFF 編碼後的 EF BB BF 這 3 個位元組。在 UTF-16 中它有標示位元組順序(哪個位元組在前)的實用角色,但 UTF-8 沒有位元組順序的問題,所以它純粹只作為「這是 UTF-8 喔」的記號。它不是必須的,加上去反而可能讓沒有預期開頭 3 個位元組的程式出錯。另一方面,Excel 辨識 CSV 時 BOM 卻很管用 — 這場實務上的拔河會在第 6 章處理。
本章帶走的重點
- UTF-8 是 1〜4 位元組的可變長度。開頭位元組中 1 的個數報出位元組數,後續一定以 10 開頭
- U+3042 → 0011 / 000001 / 000010 → E3 81 82。轉換只是位元的重新裝箱
- ASCII 在 UTF-8 中同樣是 1 個位元組,多位元組字元的組成位元組全都在 0x80 以上 — 這 2 點是 ASCII 相容的核心
- BOM 是 U+FEFF 編碼後的 EF BB BF。在 UTF-8 中是非必須的「記號」
下一章來看比 UTF-8 更早支撐日文的 Shift_JIS。你會明白 UTF-8「多位元組字元中間不會出現 ASCII 值」的安心,是多麼珍貴。