小村軟體有限公司
第 6 章

實務上的陷阱 — 讓宣告與實體一致

亂碼的真面目是「宣告(標籤)」與「實體(位元組序列)」的不一致。收拾 charset 宣告、Excel 與帶 BOM 的 UTF-8、換行字元、資料庫連線設定這些現場的經典問題。

「昨天還是正常的」— 亂碼發生在邊界上

在開發環境一切正常,上了正式環境頁面卻亂掉。批次輸出的 CSV,使用者用 Excel 一開就亂。從應用程式看起來登錄得好好的,直接看 DB 卻存著別的字元。— 實務上的亂碼,大多發生在系統與系統的邊界,也就是資料交接的地方。

本章的工具只有一個。跨越邊界的資料,有實體(位元組序列),以及告知該如何讀它的宣告(標籤)。而宣告不會替你轉換位元組序列,它只是一張標籤。標籤與內容物錯位的那一瞬間,接收方就會查錯對應表,第 1 章看到的亂碼就此重演。

顯示在 UTF-8 的實體貼上 charset=Shift_JIS 的標籤送出後,瀏覽器相信標籤而誤讀導致亂碼的路徑圖

實體是 UTF-8,標籤是 Shift_JIS。瀏覽器相信標籤,所以亂掉。該修的是標籤還是實體 — 兩種可能都要懷疑,這是調查的固定型。

Web 的宣告 — Content-Type 與 meta charset

網頁的編碼標籤有 2 個位置:HTTP 回應標頭的 Content-Type: text/html; charset=UTF-8,以及 HTML 內的 <meta charset="UTF-8">。兩者都存在時,HTTP 標頭優先 — 也就是說,伺服器的設定可以蓋過寫在檔案裡的 meta。

「檔案轉成 UTF-8 了、meta 也改了,卻只有正式環境會亂」這種怪現象,多半是伺服器或框架在標頭上掛著舊的 charset。反過來,如果哪裡都沒有宣告,瀏覽器就用猜的來讀。把實體轉成 UTF-8,並讓標頭與 meta 兩邊都對齊 UTF-8 — 這是現代 Web 的基本形。

Excel 與 CSV — 「帶 BOM 的 UTF-8」派上用場的場合

CSV 沒有 charset 標頭那樣的宣告機制。被雙擊開啟時,傳統的 Excel 會當作地區設定的預設值(日文環境是 CP932)去讀。UTF-8 輸出的 CSV 會亂掉的經典原因就是這個。

這時第 3 章的 BOM 就派上用場了。只要檔案開頭帶著 EF BB BF,Excel 就會以它為記號、當作 UTF-8 來讀。「系統輸出的 CSV 做成帶 BOM 的 UTF-8」是給 Excel 使用者開啟的檔案的定石。但它並非萬能 — 把同一個檔案交給沒有預期 BOM 的其他系統,開頭 3 個位元組就成了多餘的資料,可能招致誤動作。看誰要讀這個檔案,決定 BOM 的去留,是實務上的判斷。

小題 6-1 — 宣告與實體

在 Web 與 Excel 的經典場景中,確認「標籤不會轉換位元組序列」這個原則。

Q1. HTTP 的 Content-Type 標頭中的 charset,以及 HTML 的 meta charset 的角色,正確的是哪一個?

Q2. 以 UTF-8 儲存的 HTML,被宣告成 charset=Shift_JIS 發佈,出現了亂碼。對亂碼「真面目」最精確的說明是哪一個?

Q3. 以 UTF-8(無 BOM)輸出的 CSV,使用者雙擊用 Excel 打開後日文出現亂碼,這是經典麻煩。檔案端具代表性的對策是哪一個?

換行字元 — 另一種「看不見的位元組」

接到編碼相關的諮詢,最後發現犯人其實是換行字元 — 這種事屢見不鮮。換行也是位元組。Windows 慣用 CRLF(0D 0A)共 2 個位元組,Unix/Linux 與 macOS 慣用 LF(0A)共 1 個位元組,「看起來」相同的文字,位元組序列卻不同。

把 CRLF 的檔案交給以 LF 為前提的程式,每一行的行尾都會殘留 CR(0x0D),造成「值的尾端多了看不見的 1 個字元導致比對失敗」「行數的算法錯開」這類比亂碼更難察覺的問題。在 CSV 介接的約定中,編碼與換行字元成套指定(例:UTF-8、無 BOM、LF)是定石。

資料庫 — 宣告在路徑的每一段都必要

資料庫的編碼設定也有好幾處。代表性的是資料表與欄位的編碼(位元組序列如何儲存與解讀),以及連線的編碼(應用程式與 DB 的對話用什麼進行的宣告)。在應用程式 → 連線 → 資料表這條路徑上,只要有任何一段宣告與實體錯位,儲存或取出時就會發生錯誤轉換、錯誤解讀,亂掉的資料就這樣被存了下來。

棘手的是,與網頁不同,亂掉的結果會被永久保存。第 1 章的鐵則「不要在亂掉的狀態下儲存」的最糟形態,就是在 DB 裡靜靜累積的亂碼資料。新建系統時一開始就把全部路段統一成 UTF-8 系;既有系統則一段一段確認「哪一段的設定與實體錯位」— 調查的型在這裡也一樣。

本課程的口訣,實務版:亂掉時要問「在哪個邊界上,宣告與實體錯位了」。寫入的編碼與讀取的編碼的錯位,有多少個邊界就可能發生多少次。

小題 6-2 — 看不見的位元組與連線設定

換行字元與資料庫 — 為「看不見的部分」的不一致做個收尾。

Q4. 關於換行字元的說明,正確的是哪一個?

Q5. 資料庫發生亂碼的典型原因,正確的是哪一個?

Q6. 有沒有辦法在讀取之前,100% 確定收到的文字檔是哪種編碼?

本章帶走的重點

  • charset 是標籤,位元組序列不會被轉換。HTTP 標頭優先於 meta
  • 會被 Excel 開啟的 CSV,定石是帶 BOM 的 UTF-8。但 BOM 的去留要看「誰來讀」決定
  • 換行字元(CRLF 0D 0A / LF 0A)是與編碼不同軸的「看不見的位元組」問題。約定要成套訂
  • DB 需要資料表編碼與連線編碼在全部路段一致。亂碼被永久保存是最糟的模式
  • 沒有百分之百判定編碼的方法。把它定成規格、讓宣告與實體一致才是本丸

工具全部到齊了。最終章是把亂掉的畫面本身當作證據、推理誤讀組合的綜合演習。