亂碼是怎麼產生的?ASCII、Unicode、UTF-8 字元編碼完整指南

你曾經打開一個文字檔,卻看到滿屏的「锟斤拷烫烫烫」或「â¥人」嗎?或者在處理 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 個字元,完全無法容納歐洲各國語言的重音字母,更別說中文、日文、阿拉伯文了。

為什麼大寫 A 是 65?
設計者 Bob Bemer 選擇讓大小寫字母之間相差 32(即第 6 個位元的差異),這樣只需翻轉一個位元就能在大小寫之間切換——這個設計至今仍體現在 Python 的 ord('A') == 65ord('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英文1281全球(英文)
ISO 8859-1西歐語言2561歐洲
Big5繁體中文13,0602臺灣、香港
GBK簡體中文21,0032中國大陸
Shift-JIS日文約 6,8791–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 → 大寫字母 A
  • U+4E2D → 漢字「中」
  • U+1F600 → 😀(笑臉 Emoji)
  • U+1F4A9 → 💩(便便 Emoji)

目前 Unicode 14.0 定義了超過 144,697 個字元,涵蓋 159 種文字系統,從古埃及象形文字到現代 Emoji,一套標準統管全部。

Unicode ≠ UTF-8
這是最常見的混淆點。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-81–4(可變)網路傳輸、檔案儲存
UTF-162 或 4有(需 BOM)Windows 內部、Java 字串
UTF-324(固定)內部處理、快速索引

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。

立即使用 URL 編碼/解碼工具

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"
MySQL 的 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 字元,安全傳輸後再解碼

立即使用文字轉換工具   Base64 編解碼工具

11. 小結

字元編碼的演進史,是一部技術標準從分裂走向統一的歷史:

  • ASCII(1963):128 個字元,奠定英文數位化基礎,至今仍是所有現代編碼的子集
  • 分裂時代(1970–1990s):各地區自行擴充,Big5、GBK、Shift-JIS 並立,互不相容,亂碼頻發
  • Unicode(1991+):統一字元集,為世界上所有文字指定唯一碼位
  • UTF-8(1992+):Unicode 的最佳編碼方式,相容 ASCII、空間效率高、無位元組序問題,成為全球 97% 網頁的標準

今天,只要你在整個技術堆疊(檔案、程式碼、資料庫、HTTP Headers)中統一使用 UTF-8,亂碼問題幾乎不會出現。而當你看到亂碼時,也能快速診斷:找出哪個環節的編碼聲明與實際儲存不一致,修正它,問題就迎刃而解。