實務上的陷阱 — 讓宣告與實體一致
亂碼的真面目是「宣告(標籤)」與「實體(位元組序列)」的不一致。收拾 charset 宣告、Excel 與帶 BOM 的 UTF-8、換行字元、資料庫連線設定這些現場的經典問題。
「昨天還是正常的」— 亂碼發生在邊界上
在開發環境一切正常,上了正式環境頁面卻亂掉。批次輸出的 CSV,使用者用 Excel 一開就亂。從應用程式看起來登錄得好好的,直接看 DB 卻存著別的字元。— 實務上的亂碼,大多發生在系統與系統的邊界,也就是資料交接的地方。
本章的工具只有一個。跨越邊界的資料,有實體(位元組序列),以及告知該如何讀它的宣告(標籤)。而宣告不會替你轉換位元組序列,它只是一張標籤。標籤與內容物錯位的那一瞬間,接收方就會查錯對應表,第 1 章看到的亂碼就此重演。
實體是 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 的角色,正確的是哪一個?
charset 宣告終究只是「讀法的指示書」,不會碰位元組序列本身。所以若在 UTF-8 的實體上寫 charset=Shift_JIS,瀏覽器就會一板一眼地用 Shift_JIS 的表去查,然後亂掉。「改了宣告還是沒好」時是實體那邊、「轉換了實體還是亂」時是宣告那邊停留在舊狀態 — 這個切分方法可以直接拿來用。
Q2. 以 UTF-8 儲存的 HTML,被宣告成 charset=Shift_JIS 發佈,出現了亂碼。對亂碼「真面目」最精確的說明是哪一個?
這正是第 1 章的固定套路 — 亂碼的真面目永遠是「寫入的編碼」與「讀取的編碼」的錯位,這次是宣告誤導了讀法。位元組序列沒有損壞,把標籤改成與實體一致就解決了。不要用「八字不合」或「bug」這種話帶過,能說出是哪一邊錯位 — 這就是本課程的目標。
Q3. 以 UTF-8(無 BOM)輸出的 CSV,使用者雙擊用 Excel 打開後日文出現亂碼,這是經典麻煩。檔案端具代表性的對策是哪一個?
傳統的 Excel 對雙擊開啟、又沒有編碼宣告的 CSV,會用地區設定的預設值(日文環境是 CP932)去讀。在開頭加上 BOM,它就成為「這是 UTF-8」的記號,能被正確讀取。第 3 章學過「BOM 是非必須的記號」,而 Excel 介接正是這個記號在實務上發揮威力的代表例。反過來,對沒有預期 BOM 的匯入程式,開頭 3 個位元組反而礙事 — 要看對象決定加或不加。
換行字元 — 另一種「看不見的位元組」
接到編碼相關的諮詢,最後發現犯人其實是換行字元 — 這種事屢見不鮮。換行也是位元組。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. 關於換行字元的說明,正確的是哪一個?
換行也是正正經經的位元組:CR(0x0D)與 LF(0x0A)。Windows 系慣用 CRLF 共 2 個位元組,Unix 系慣用 LF 共 1 個位元組,於是以行為單位處理檔案的程式或 shell 腳本,就會因「行尾多了/少了 1 個位元組」而出錯。這雖然不是字元集的問題,但就「看不見的部分的位元組因環境而異」這一點來說,是編碼麻煩的近親,在 CSV 介接中應該與編碼成套確認。
Q5. 資料庫發生亂碼的典型原因,正確的是哪一個?
資料庫有「資料表與欄位的編碼」和「連線的編碼(用戶端用什麼溝通的宣告)」等多項設定,它們的不一致是亂碼的典型原因。應用程式明明送出 UTF-8 的位元組序列、連線設定卻停留在 latin1 之類的組態,會在 DB 內部上演「存入時與取出時讀法錯位」。與 Web 伺服器的 charset 一樣,宣告與實體的一致在路徑的每一段都是必要的。
Q6. 有沒有辦法在讀取之前,100% 確定收到的文字檔是哪種編碼?
編碼的自動判定是「這串位元組序列用哪張表讀起來比較自然」的推測,遇到很短的檔案、或可以被多種編碼解讀的位元組序列時就會失手(著名的例子中,自古就知道有依內容而誤判的情況)。所以實務的本丸不是判定技術而是約定 — 在介面規格書中明記「UTF-8(無 BOM)、換行 LF」,在能宣告的地方(HTTP 標頭、DB 連線設定)一律宣告。亂碼的推理(第 7 章)則是那些約定被打破時的調查術。
本章帶走的重點
- charset 是標籤,位元組序列不會被轉換。HTTP 標頭優先於 meta
- 會被 Excel 開啟的 CSV,定石是帶 BOM 的 UTF-8。但 BOM 的去留要看「誰來讀」決定
- 換行字元(CRLF 0D 0A / LF 0A)是與編碼不同軸的「看不見的位元組」問題。約定要成套訂
- DB 需要資料表編碼與連線編碼在全部路段一致。亂碼被永久保存是最糟的模式
- 沒有百分之百判定編碼的方法。把它定成規格、讓宣告與實體一致才是本丸
工具全部到齊了。最終章是把亂掉的畫面本身當作證據、推理誤讀組合的綜合演習。