小村軟體有限公司
第 3 章

親手組出 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+007F10xxxxxxx
U+0080〜U+07FF2110xxxxx 10xxxxxx
U+0800〜U+FFFF31110xxxx 10xxxxxx 10xxxxxx
U+10000〜U+10FFFF411110xxx 10xxxxxx 10xxxxxx 10xxxxxx

模板中黑色的部分(111010)是固定的「印記」。開頭位元組中 1 的個數報出該字元的位元組數,後續位元組一定以 10 開頭 — 光憑這條規則,就能從位元組序列的任何位置找到字元的分界。

手算 — 把 U+3042 變成 3 個位元組

日文字元「あ」的 U+3042 落在 U+0800〜U+FFFF 的範圍,所以使用 3 位元組的模板。步驟只有 2 步。

Step 1 — 把號碼換成二進位,從右邊以 6、6 切開
0x3042 = 0011 0000 0100 0010。從右邊每 6 個位元切開,得到 0011 / 000001 / 000010(4 + 6 + 6 個位元)
Step 2 — 從前面填進模板的 x
11100011 10000001 10000010 → 換成十六進位就是 E3 81 82

驗算看看。E3 = 1110 0011 — 開頭有 3 個 1,所以是「3 位元組字元的開頭」。81 = 1000 0001、82 = 1000 0010 — 都以 10 開頭,是「後續位元組」。把印記剝掉、剩下的位元接起來,就是 0011 000001 000010 = 0x3042,回到原點。來回都只是位元的重新裝箱

把 U+3042 的 16 個位元切成 4、6、6,灌進 1110xxxx 10xxxxxx 10xxxxxx 的模板得到 E3 81 82 的圖

碼位的位元被灌進模板的樣子。上色的位元是「あ」號碼的內容,黑色部分是 UTF-8 固定的印記。

小題 3-1 — 灌進模板

請照正文的步驟,在紙上寫下二進位邊算邊確認。

Q1. 從頭讀取 UTF-8 的位元組序列時,要怎麼知道某個位元組是「幾位元組字元的開頭」?

Q2. 把 U+3042(二進位為 0011 0000 0100 0010)灌進 3 位元組模板 1110xxxx 10xxxxxx 10xxxxxx 的正確步驟是哪一個?

Q3. ASCII 字元在 UTF-8 中是 1 個位元組,漢字與日文平假名多數是 3 個位元組。字串「Hello世界」以 UTF-8 表示共是幾個位元組?

位元組

為什麼 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 相容」能安全成立的原因,正確的是哪一個?

Q5. 日文的「こんにちは」(5 個平假名,每個都是 3 個位元組)以 UTF-8 表示共是幾個位元組?

位元組

Q6. 關於 UTF-8 的 BOM(Byte Order Mark),正確的說明是哪一個?

本章帶走的重點

  • 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 值」的安心,是多麼珍貴。