你曾經打開一個文字檔,卻看到滿屏的「锟斤拷烫烫烫」或「â¥人」嗎?或者在處理 API 資料時,中文全部變成「??」?這些惱人的問題幾乎全都源自同一個原因:字元編碼不一致。搞懂字元編碼,不只是解決技術問題,更是理解電腦如何處理文字的基礎。
1. 文字與數字:電腦如何儲存文字?
電腦在底層只認識 0 與 1——所有資料都以二進位表示。要儲存文字,就必須建立一套「文字到數字」的對應表,告訴電腦「65 代表大寫字母 A」、「20013 代表『中』這個漢字」。這套對應規則就是字元編碼(Character Encoding)。
字元編碼包含兩個部分:
- 字元集(Character Set):定義「有哪些字元」以及每個字元的編號(碼位,Code Point)
- 編碼方式(Encoding):定義「如何用位元組序列來表示這個編號」
理解這個區別很重要:Unicode 是字元集(定義了超過 14 萬個字元的編號),而 UTF-8 則是其中一種編碼方式(規定如何把這些編號轉成位元組)。
2. ASCII:萬碼之祖
1963 年,美國國家標準協會(ANSI)發布了 ASCII(American Standard Code for Information Interchange),這是現代字元編碼的起點。
ASCII 使用 7 個位元(bit),共定義了 128 個字元(0–127):
- 0–31:控制字元(如換行 LF=10、回車 CR=13、Tab=9)
- 32–47:標點符號和空格
- 48–57:數字 0–9
- 65–90:大寫英文字母 A–Z
- 97–122:小寫英文字母 a–z
ASCII 的設計非常適合英語,但它只有 128 個字元,完全無法容納歐洲各國語言的重音字母,更別說中文、日文、阿拉伯文了。
設計者 Bob Bemer 選擇讓大小寫字母之間相差 32(即第 6 個位元的差異),這樣只需翻轉一個位元就能在大小寫之間切換——這個設計至今仍體現在 Python 的
ord('A') == 65 和 ord('a') == 97 中。
3. 各自為政的時代:Big5、GB2312 與 ISO 8859
ASCII 空間不足,世界各地的組織開始自行擴充。這導致了一段「各自為政」的時代,也是大多數亂碼問題的根源。
3.1 歐洲:ISO 8859 系列
ISO 8859 利用第 8 個位元(ASCII 未使用),將字元範圍從 128 擴展到 256,加入各地語言的重音字元。但問題在於,ISO 8859 分成了 16 個子集(如 ISO 8859-1 覆蓋西歐語言、ISO 8859-7 覆蓋希臘語),不同子集中相同的數字代表不同的字元。
3.2 繁體中文:Big5
Big5 由臺灣資策會於 1984 年制定,使用兩個位元組(2 bytes)表示一個中文字,共收錄約 13,060 個常用及次常用漢字。Big5 長期主導臺灣、香港的繁體中文環境,早期 Windows 的繁體版本預設使用 Big5(代碼頁 950)。
3.3 簡體中文:GB2312 與 GBK
中國大陸於 1981 年制定 GB2312,收錄 6,763 個漢字。1993 年擴充為 GBK,收錄 21,003 個漢字,後來進一步擴充為 GB18030。Windows 簡體中文版預設使用 GBK(代碼頁 936)。
3.4 日文:Shift-JIS 與 EUC-JP
日文編碼同樣經歷了類似的分裂局面,主要有 Shift-JIS(Windows 日文版預設)和 EUC-JP(Unix/Linux 系統常用)兩種主流格式,互不相容。
| 編碼 | 適用語言 | 字元數 | 位元組/字 | 主要使用地區 |
|---|---|---|---|---|
| ASCII | 英文 | 128 | 1 | 全球(英文) |
| ISO 8859-1 | 西歐語言 | 256 | 1 | 歐洲 |
| Big5 | 繁體中文 | 13,060 | 2 | 臺灣、香港 |
| GBK | 簡體中文 | 21,003 | 2 | 中國大陸 |
| Shift-JIS | 日文 | 約 6,879 | 1–2 | 日本 |
| UTF-8 | 所有語言 | 140,000+ | 1–4 | 全球 |
4. Unicode:終結分裂的統一標準
1987 年,來自 Xerox 的 Joe Becker 和來自 Apple 的 Lee Collins、Mark Davis 開始了一個野心勃勃的計畫:建立一套能涵蓋世界上所有文字的統一字元集。1991 年,Unicode 1.0 正式發布。
Unicode 為每個字元指定一個唯一的碼位(Code Point),以 U+ 加十六進位表示:
U+0041→ 大寫字母 AU+4E2D→ 漢字「中」U+1F600→ 😀(笑臉 Emoji)U+1F4A9→ 💩(便便 Emoji)
目前 Unicode 14.0 定義了超過 144,697 個字元,涵蓋 159 種文字系統,從古埃及象形文字到現代 Emoji,一套標準統管全部。
這是最常見的混淆點。Unicode 是「字元編號的標準」(每個字元有一個唯一的碼位數字),而 UTF-8 是「如何用位元組儲存這些數字的方式」。UTF-8 是 Unicode 的其中一種編碼方式,另外還有 UTF-16 和 UTF-32。
5. UTF-8:為什麼它成為全球標準?
Unicode 確立了每個字元的編號,但需要決定如何用位元組來儲存這些編號。UTF-8 由 Ken Thompson 和 Rob Pike 於 1992 年設計,它的核心思想極為優雅:可變長度編碼。
5.1 UTF-8 的編碼規則
UTF-8 根據碼位的大小,使用 1 到 4 個位元組:
- 1 位元組(0xxxxxxx):涵蓋 U+0000 至 U+007F,即完整的 ASCII 範圍——UTF-8 與 ASCII 完全向下相容
- 2 位元組(110xxxxx 10xxxxxx):涵蓋 U+0080 至 U+07FF,包含大多數歐洲語言字元
- 3 位元組(1110xxxx 10xxxxxx 10xxxxxx):涵蓋 U+0800 至 U+FFFF,包含中日韓文字
- 4 位元組(11110xxx 10xxxxxx 10xxxxxx 10xxxxxx):涵蓋 U+10000 以上,包含 Emoji 和古文字
5.2 UTF-8 勝出的原因
UTF-8 之所以成為全球主流(目前佔全球網頁 97% 以上),關鍵在於幾個設計優勢:
- 向下相容 ASCII:純英文內容在 UTF-8 和 ASCII 下完全相同,不需要任何轉換,已有大量舊系統無需修改即可相容
- 自同步性:每個多位元組序列的開頭位元組都有獨特的標記(11xxxxxx),續接位元組以 10 開頭,可以在串流中的任意位置開始解碼,不會因為截斷而誤判
- 無位元組序問題:UTF-16 和 UTF-32 會有大小端(Endianness)問題,UTF-8 是以位元組為單位,沒有這個困擾
- 空間效率均衡:英文內容每字元只需 1 個位元組(與 ASCII 相同),中文每字元 3 個位元組(比 UTF-16 的固定 2 或 4 位元組更彈性)
6. UTF-16 與 UTF-32:何時使用?
6.1 UTF-16
UTF-16 使用 2 或 4 個位元組表示字元(基本多語言平面 BMP 的字元用 2 個位元組,其餘用 4 個位元組的代理對 Surrogate Pair)。Windows NT 核心、Java 和 JavaScript 的字串內部表示都使用 UTF-16。
UTF-16 的主要問題是不相容 ASCII(英文字母也需要 2 個位元組),且有大小端問題(需要 BOM 標記位元組序)。
6.2 UTF-32
UTF-32 固定使用 4 個位元組表示每個字元,優點是隨機存取極為簡單(字元 n 的位置就是偏移 n×4),缺點是空間浪費最嚴重(英文文字的儲存空間是 UTF-8 的 4 倍)。主要用於需要快速索引特定字元位置的內部處理。
| 編碼 | 位元組數/字元 | 相容 ASCII | 位元組序問題 | 主要用途 |
|---|---|---|---|---|
| UTF-8 | 1–4(可變) | 是 | 無 | 網路傳輸、檔案儲存 |
| UTF-16 | 2 或 4 | 否 | 有(需 BOM) | Windows 內部、Java 字串 |
| UTF-32 | 4(固定) | 否 | 有 | 內部處理、快速索引 |
7. 亂碼是怎麼產生的?
現在你已經了解字元編碼的基礎,可以解析亂碼的本質了:亂碼就是用錯誤的編碼方式解讀位元組序列。幾個典型場景:
7.1 Big5 檔案用 UTF-8 開啟
Big5 中的「中文」(3 個漢字),儲存的位元組是 A4 A4 A4 E5(繁體「中文」兩字)。若用 UTF-8 解讀,這些位元組不構成合法的 UTF-8 序列,就會顯示為「â–»」之類的亂碼,或直接顯示替代字元 ?。
7.2 GB2312 與 Big5 互相誤讀
Big5 和 GB2312 的字元空間有重疊——同樣的位元組值,在 Big5 中代表繁體字,在 GB2312 中代表完全不同的簡體字(甚至可能根本不對應任何合法字元)。這就是早年兩岸三地交換文件時最常遇到的問題。
7.3 缺少 BOM 的 UTF-16 檔案
UTF-16 需要 BOM(Byte Order Mark,位元組順序標記)來指示是大端(UTF-16 BE)還是小端(UTF-16 LE)。如果 BOM 遺失,軟體可能用錯誤的位元組序解讀,導致每個字元都是亂碼。
7.4 資料庫 / API 的「??」問題
如果你的 MySQL 資料庫連線字元集設為 latin1,但存入的資料是 UTF-8 編碼的中文,資料庫會把無法解讀的位元組轉成 ?,且這個過程不可逆——原始資料就此永久損壞。
這是最著名的亂碼之一,通常出現在 GBK 環境下誤讀了 UTF-8 的替代字元(U+FFFD,即
EF BF BD)。這三個位元組在 GBK 中剛好解讀成「锟斤拷」,由於 UTF-8 替代字元常成對出現,最終形成「锟斤拷锟斤拷」的連鎖亂碼。「烫烫烫」則是 0xCDCD(未初始化記憶體的填充值)在 GBK 下的顯示結果。
8. URL 編碼:字元編碼的另一個戰場
URL 中只允許特定的 ASCII 字元,非 ASCII 字元(如中文)必須進行百分比編碼(Percent-Encoding)。規則是:將字元的 UTF-8 位元組序列中的每個位元組,以 %XX 的格式表示(XX 為十六進位)。
例如,「台灣」這兩個字的 UTF-8 編碼是:
- 「台」→ UTF-8 位元組
E5 8F B0→ URL 編碼%E5%8F%B0 - 「灣」→ UTF-8 位元組
E7 81 A3→ URL 編碼%E7%81%A3
所以 https://example.com/搜尋?q=台灣 在傳輸時實際上是 https://example.com/%E6%90%9C%E5%B0%8B?q=%E5%8F%B0%E7%81%A3。
早期有些系統用 Big5 或 GBK 進行 URL 編碼,導致同樣的 URL 在不同系統之間解讀結果不一致。現代標準(RFC 3986)明確規定 URL 編碼應基於 UTF-8。
9. 如何從根本避免亂碼?
解決亂碼的根本方法,是在整個系統鏈的每個環節都統一使用 UTF-8:
9.1 文字檔與程式碼
- 儲存所有文字檔時指定 UTF-8(無 BOM)
- 在 HTML 的
<head>最頂端加上<meta charset="UTF-8"> - PHP 檔案本身以 UTF-8 編碼儲存,並在 HTML 頭部宣告
charset
9.2 資料庫
- 建立資料庫和資料表時指定
CHARACTER SET utf8mb4(注意:MySQL 的utf8其實是殘缺的 3 位元組 UTF-8,無法儲存 Emoji 等 4 位元組字元,應使用utf8mb4) - 資料庫連線時設定
SET NAMES utf8mb4或在 PDO 連線字串中加入charset=utf8mb4
9.3 HTTP Headers
- 伺服器回應時加入
Content-Type: text/html; charset=utf-8 - 接收 POST 資料前確認表單的
accept-charset="UTF-8"
utf8 陷阱MySQL 歷史上有個著名的 Bug:
utf8 字元集只支援最多 3 個位元組的 UTF-8 字元,而完整的 UTF-8 最多需要 4 個位元組(用來表示 Emoji 和一些罕見漢字)。因此,若你的應用需要儲存 Emoji(如 😀),必須使用 utf8mb4 而非 utf8,否則 Emoji 會被截斷並導致插入失敗或資料損壞。
10. 文字轉換工具的實際應用
了解字元編碼後,可以用工具做什麼?幾個實際場景:
- 全形/半形轉換:日文輸入法常會產生全形數字(123)和全形英文(ABC),它們在 Unicode 中是獨立的碼位(U+FF10 等),需要工具轉換為標準半形字元
- 繁簡轉換:Big5 和 GBK 收錄的漢字字形不同,部分繁體字在簡體中文字型下顯示有差異,轉換時需要字形映射表,而非僅僅更換編碼
- Base64 編碼:在無法傳輸原始二進位的管道(如某些舊式電子郵件系統)中,Base64 可以將任意位元組序列(包括 UTF-8 編碼的文字)轉成純 ASCII 字元,安全傳輸後再解碼
11. 小結
字元編碼的演進史,是一部技術標準從分裂走向統一的歷史:
- ASCII(1963):128 個字元,奠定英文數位化基礎,至今仍是所有現代編碼的子集
- 分裂時代(1970–1990s):各地區自行擴充,Big5、GBK、Shift-JIS 並立,互不相容,亂碼頻發
- Unicode(1991+):統一字元集,為世界上所有文字指定唯一碼位
- UTF-8(1992+):Unicode 的最佳編碼方式,相容 ASCII、空間效率高、無位元組序問題,成為全球 97% 網頁的標準
今天,只要你在整個技術堆疊(檔案、程式碼、資料庫、HTTP Headers)中統一使用 UTF-8,亂碼問題幾乎不會出現。而當你看到亂碼時,也能快速診斷:找出哪個環節的編碼聲明與實際儲存不一致,修正它,問題就迎刃而解。