釐清 Windows 的文字編碼 - 亂碼為什麼會發生,尤其是與 Linux 搭配時什麼地方會偏掉
· 小村 豪 · Windows, 亂碼, UTF-8, CP932, Linux, PowerShell, Unicode
Windows 的亂碼,並不是因為日文難處理才發生。絕大多數是因為 把同一串 bytes 當成另一種編碼去讀,或 把錯誤讀到的結果又以另一種編碼存回去。
尤其當 Windows 與 Linux 同時參與時,Windows 側還並存 CP932、UTF-8、UTF-16、console 的 code page、PowerShell 各版本差異等等多重脈絡,而 Linux 側多半以 UTF-8 為前提,於是平時沒顯現的前提差異就會一次全部浮上檯面。
這其實不是「日文處理難」的問題,而是 同一組 bytes 是用什麼前提在處理 的問題。本文就從「亂碼為什麼會發生」的角度整理 Windows 的文字編碼,再把與 Linux 搭配時特別容易出事的點聚焦一下。
1. 先抓住的重點
先把要點列出來,最重要的是下列 6 項:
- 亂碼不是「文字」的問題,是「bytes 被怎麼解釋」的問題。
- Windows 上 Unicode 系與 legacy code page 系並存,同一台機器裡不同脈絡的前提就不同。
- Linux 側以 UTF-8 為前提居多,所以摻進 Windows 側的 CP932 或 UTF-16 就容易出事。
- 只是顯示壞掉 與 把壞掉的內容也存回去 要分開想。
- 新檔以 UTF-8 為第一首選,既有 legacy 檔在有明確遷移任務之前先維持原狀 是最安全的。
- 檔案的 encoding、編輯器的 encoding、console 的 code page、應用內部的字串格式是不同層。混在一起講,調查就會迷路。
單說「在 Windows 上亂碼了」無法定位原因。至少要分清楚下列哪一層偏掉:
- 檔案本身的編碼
- 儲存時的編碼
- 編輯器的解讀
- console 的 input/output code page
- 應用程式內部的字串格式
- Linux 側的 locale 與預期編碼
2. 亂碼的本質
亂碼的本質其實很單純:
- 把字串以某種編碼 encode 成 bytes
- 再把那串 bytes 以某種編碼 decode 回字串
- encode 與 decode 的前提不一致,就會讀成不同的字串
舉例:あ 以 UTF-8 儲存,bytes 是:
E3 81 82
把這串 bytes 當 UTF-8 讀是 あ,用 CP932 脈絡讀就會變成 縺� 這樣的另一個字串。這就是亂碼。
重點是:這裡發生的不是「日文壞了」,而是 同一組 bytes 的解釋錯位。
2.1 只是顯示壞掉還有機會救
亂碼其實有「還救得回來」的階段。只要 原本的 bytes 沒被改動,之後用正確的編碼重新開就有機會還原。
反過來危險的流程是這樣:
- 把 UTF-8 檔案誤當 CP932 讀
- 畫面上看起來像
縺� - 就這樣把「看到的字串」存起來
- 原本的 UTF-8 bytes 被覆蓋掉
走到這一步,就不是顯示問題,而是 資料毀損。
2.2 更危險的,是把「表現不出的字」塞進窄 code page
另一種典型事故,是把 Unicode 字串往 CP932 這種 legacy code page 寫。
若對方 code page 裡沒有那個字,會出現:
- 被換成
? - 被塞
� - 被換成相近的別字
- 轉換失敗
這種事故要看的不只是「讀不讀得到」,而是 來回轉一次還能還原嗎。一旦失掉的字元,後面就算知道正確編碼也還不回來。
3. 為什麼 Windows 上容易變複雜
Windows 之所以複雜,不是因為它「老」,而是 Unicode 的世界與 legacy code page 的世界至今仍共存。
3.1 Windows API 同時有 Unicode 系與 code page 系
Windows API 大致分兩套:
W系:wide character,以 UTF-16 處理 UnicodeA系:ANSI,走當時的 active code page
也就是說,Windows 本身就同時存在「以 Unicode 處理的路」與「以當下 active code page 處理的路」。所以在同一台 Windows 上,走哪條 API 或哪個工具,前提就不一樣。
3.2 「Windows 的日文」不只一個
在 Windows 日文環境上,實務上常見混用的有 4 種:
- CP932:日文 Windows 的 legacy 文字常見
- UTF-8:新的文字資產、web、跨平台工具上越來越多
- UTF-16LE:Windows 系工具或 API 脈絡中仍然常見
- console 的 code page:
cmd.exe或一部分 console 工具在輸入/輸出時的另一層
要注意:chcp 65001 不會讓檔案也變成 UTF-8。console 的 code page 和既有檔案是什麼 bytes,是兩件事。
另外,把日文 Windows 的 legacy 文字粗略叫「Shift_JIS」的人很多,但實務上用 CP932 這個名字比較不會聊歪,至少明確在講「這是 Windows 家的日文 legacy 編碼」。
3.3 檔名與檔案內容是不同問題
Windows 上日文檔名看起來沒事,很容易就誤以為「內容也沒事」。這就是陷阱。
- 處理 path/file name 的層
- 讀檔案內容的層
- 顯示到 console 的層
這 3 個層要分開看。
例如日文 path 完全沒問題,但檔案內容是 CP932 存的,被 Linux 當 UTF-8 讀就炸。反之,檔案內容是 UTF-8,console 的 code page 不對也只是顯示壞。
3.4 PowerShell 與周邊工具的預設值並不一致
讓 Windows 低調地製造事故的一個原因是:「我覺得我只是輸出文字」,但走不同路徑輸出的 bytes 卻不同。
特別要留意:
- Windows PowerShell 5.1 的預設 encoding 不一致
- 部分 cmdlet 或重新導向會產生 UTF-16LE
- 另一些路徑則使用 active ANSI code page
- PowerShell 7 之後 預設為 UTF-8 no BOM
也就是說,「我是用 PowerShell 輸出的文字」本身還定不了編碼。要看是哪一版、哪個 cmdlet、走哪條寫檔路徑。
4. 與 Linux 搭配時的典型事故
Windows 單機原本看起來還好的東西,一旦夾進 Linux,就變得很容易壞。理由簡單,Linux 側 以 UTF-8 為前提的傾向很強。
4.1 Windows 以 CP932 存的檔,被 Linux 當 UTF-8 讀
最常見的事故:
- Windows 上 legacy 應用或老舊流程用 CP932 寫 CSV/TXT/log
- Linux 側的 script 或工具依 locale 預設當 UTF-8 讀
- 結果就是 decode error、
�、意義不明的日文
這時不是 Linux 工具不好,而是 這串 bytes 根本沒附上編碼這個約定。
4.2 Linux/VS Code 做的 UTF-8 no BOM 被 Windows 當 ANSI
反方向的事故:
- 在 Linux 或 VS Code 做的 UTF-8 no BOM 的 script/config/文字
- Windows PowerShell 5.1 或 legacy 工具把沒 BOM 的檔當成 ANSI 側的 code page
- 只有含日文或非 ASCII 的那幾行壞掉
這個事故不是 UTF-8 本身的錯,而是 混進了「沒 BOM 時也猜不對 UTF-8」的讀檔方。
4.3 Windows 寫 UTF-16LE,Linux 看到「這個不像文字」
這種也常見:
- Windows PowerShell 5.1 的部分輸出或 legacy 工具寫出 UTF-16LE
- Linux 側的文字工具預期是 UTF-8 的單 byte 流
- 結果變成大量 NUL byte 夾在裡面的「像是二進位的文字」
UTF-16LE 本身沒罪,但 直接丟給 Linux 的文字處理工具這個前提常常不合拍。
4.4 BOM 有無也會帶來摩擦
BOM 不是編碼本身,但在實務上影響很大:
- Windows 側某些工具有 BOM 會比較輕鬆
- Linux 側某些工具把 BOM 視為檔首的多餘 bytes
- 結果是第一欄或第一行的開頭壞掉、帶入看不到的垃圾、比較結果偏差
尤其在 UTF-8 上,同樣叫 UTF-8,有 BOM 與沒 BOM 的 bytes 完全不同。只寫「改用 UTF-8」等於把規則定了一半。
4.5 光信 console 會誤判
Windows 與 Linux 跨系統時,另一個危險區是 console:
- Windows console 有 input/output 的 code page
- Linux terminal 多半在 UTF-8 locale 下跑
- 中間還要經 WSL、SSH、container、CI,顯示路徑又多了一層
在這種狀況下以「console 能讀 → 檔案也 OK」或「console 壞掉 → 檔案壞了」去判斷很容易偏掉。看到的東西壞掉,和實際存下來的 bytes 壞掉,要分開驗。
4.6 典型事故整理成表
| 情境 | 實際的 bytes | 讀檔方預期 | 典型症狀 |
|---|---|---|---|
| Windows legacy app 存的 CSV | CP932 | Linux 側是 UTF-8 | �、decode error、看不懂的日文 |
| Linux/VS Code 做的檔 | UTF-8 no BOM | Windows PowerShell 5.1 當 ANSI | 只有日文行壞掉 |
| Windows PowerShell 5.1 某些輸出 | UTF-16LE 或 ANSI | Linux 側預期是 UTF-8 文字 | NUL byte 混入、像二進位的行為 |
| UTF-8 with BOM | UTF-8 + BOM | Unix 系工具預期 plain UTF-8 | 首欄壞掉、帶入多餘字元 |
| 只信 console 的顯示 | 檔案與 console 前提不同 | 調查者只看顯示判斷 | 切分原因時走偏 |
5. 以 4 個問題推進亂碼調查
調查亂碼時若卡住,回到下面這 4 題最有效率。
5.1 原本的 bytes 是什麼
最先要看的是「現在這個檔到底是什麼 bytes」。要看的是 bytes,不是外觀。
- UTF-8
- UTF-8 with BOM
- CP932
- UTF-16LE
- 中途被人再存一次,變成別的東西了嗎
5.2 最初是誰、用什麼前提寫的
接著鎖定「第一個寫的人」:
- Windows 上的 legacy app
- PowerShell 5.1 還是 7
- Linux 上的 script
- VS Code
- 來自 Excel 的匯出
- 某個 middleware/batch/CI
這層不釐清,推測編碼就變碰運氣。
5.3 現在是誰、用什麼前提讀的
除了寫的人,也要看 讀的人的前提:
- 編輯器是否在自動偵測
- PowerShell 看不看 BOM
- Linux 是否依 locale 當 UTF-8 處理
- library 是否走預設編碼
- 是否有明確指定
Encoding.UTF8或cp932
亂碼幾乎都發生在這裡。
5.4 讀錯的內容是否已經被存起來
最後確認 損害是否還停在「只有顯示」這一階段:
- bytes 還是原本的嗎
- 有沒有人把壞掉的顯示存回去
- diff 裡有沒有冒出
?或� - 整檔有沒有被改成別的編碼
4 題補齊之後,原因通常就很清楚。
6. 減少事故的運維規則
以下是實務面的東西。Windows 與 Linux 同時參與的案子,先把下列規則定好能省很多事故。
6.1 新檔以 UTF-8 為第一首選
新檔的 text 檔以 UTF-8 為起點最穩。但別停在這裡,BOM 的走法也要一起決定。
建議思路:
- 主要被 Linux 側讀的 text:採 UTF-8 no BOM
- 給 Windows legacy 工具或 PowerShell 5.1 讀的 script:依對方情況明確指定 BOM 有無
- 確定需要 UTF-16LE 的對象:在規格書裡寫清楚
只寫「統一 UTF-8」之後一定為了 BOM 吵架。
6.2 既有 legacy 檔在明確遷移任務之前維持原狀
既有檔若是 CP932,別趁日常修 bug 順手轉 UTF-8 比較安全。
穩當的做法:
- 既有檔 維持原 encoding/BOM/換行
- 編碼變更 獨立成遷移任務
- 轉換對象、影響範圍、下游 consumer 都確認後才統一轉換
亂碼事故很多都是從好心的「順手轉 UTF-8」開始的。
6.3 把編碼當成介面的一部分
CSV、TXT、log、設定檔、簡易 protocol,不只是內容,編碼本身就是介面。
至少要在規格裡寫下面這些:
- 這個檔是 UTF-8/CP932/UTF-16LE 的哪一個
- 若是 UTF-8,有沒有 BOM
- 換行是 LF 還是 CRLF
- 誰是 producer、誰是 consumer(Linux/Windows)
- 中間的 batch 或 ETL 是否會再存一次
只寫「以 text 傳遞」不算規格。
6.4 別信預設值,寫入時明確指定
不論程式碼還是 script,寫入時把編碼寫明最安全。
危險的想法:
- 用預設儲存就好
- OS 自己會處理吧
- console 讀得到檔也應該沒事
- 反正有 auto-detect
預設值在 Windows/Linux、PowerShell 5.1/7、編輯器、runtime 間會直接差異化。不明確指定,多半就只是剛好能動。
6.5 把 console 與檔案分開驗
這條相當有效:
- 驗 console 顯示
- 重新打開檔案驗一次
兩個步驟要分開。
chcp 或 terminal 的顯示對得上,但存檔是另一種編碼就沒用;反過來,檔案正常但 console 的顯示 code page 不對,你也會被唬。
6.6 Git 不會幫你修編碼
Git 基本上 只追 bytes。也就是說,壞掉的 bytes 也會被忠實記錄。
所以遇到:
- 什麼都沒改卻冒出超大 diff
- 只有日文行出現怪 diff
- 只有檔首壞掉
- 換行與編碼一起動
這類情況,先懷疑 重新編碼造成的事故 比懷疑內容改動更快對味。
7. 最低限度的 checklist
Windows 與 Linux 混合的案子,一開始就先固定下列 checklist:
7.1 編輯前
- 這個檔目前是什麼編碼
- 有沒有 BOM
- 換行是 LF 還是 CRLF
- 代表性的日文行記 2〜3 條
- 最終 consumer 是 Linux 還是 Windows
7.2 編輯中
- 是否依賴預設編碼在寫檔
- 是不是靠 auto-detect 在 save
- PowerShell 或 shell redirection 的路徑是不是隨便用
- 「顯示能讀」就放心,這種心態是不是冒出來了
7.3 編輯後
- 存完後重新開來驗了嗎
- Linux 與 Windows 兩邊的代表行都沒壞吧
- diff 有沒有多出
?或� - 是不是只壞首行或首欄
- 是不是只有 BOM/換行的大 diff
7.4 該做為遷移任務處理的
- CP932 → UTF-8 的統一轉換
- UTF-8 BOM policy 的統一
- 以 PowerShell 5.1 為前提的 script 盤點
- CI/container/WSL/SSH 經過的 text 通道書面化
- editor/formatter/batch 的儲存設定統一
8. 總結
要用一句話說 Windows 的文字編碼問題:Unicode 的世界與 legacy code page 的世界,在今天仍舊共存。
而與 Linux 搭配時會特別容易出事,是因為 Linux 側常以 UTF-8 為前提,Windows 側的 CP932、UTF-16、console code page、PowerShell 版本差異一次被攤出來。
要記住的 5 點:
- 亂碼是 bytes 解釋錯位
- 顯示壞掉與資料毀損是兩件事
- 在 Windows 上把檔案/editor/console/API 分層思考
- 與 Linux 交換的 text 以 UTF-8 為第一首選
- 既有 legacy 檔的轉換要和一般修改分開
光說「Windows 上亂碼」範圍太大,但換成:
- 原本的 bytes 是什麼
- 誰怎麼寫
- 誰怎麼讀
- 是否已經存下去了
這 4 題一切,就能收斂很多。
文字編碼看似瑣碎,但在 Windows 與 Linux 之間它就是 I/O 契約本身。把這裡釘清楚,是最有效的對策。
9. 參考資料
Windows / Microsoft
-
[Code Pages - Win32 apps Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/intl/code-pages) -
[Code Page Identifiers - Win32 apps Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/intl/code-page-identifiers) -
[Unicode in the Windows API - Win32 apps Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/intl/unicode-in-the-windows-api) -
[Console Code Pages - Windows Console Microsoft Learn](https://learn.microsoft.com/en-us/windows/console/console-code-pages) -
[chcp Microsoft Learn](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/chcp) -
[Use UTF-8 code pages in Windows apps Microsoft Learn](https://learn.microsoft.com/en-us/windows/apps/design/globalizing/use-utf8-code-page)
PowerShell / VS Code
-
[about_Character_Encoding Microsoft Learn](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_character_encoding) -
[Understanding file encoding in VS Code and PowerShell Microsoft Learn](https://learn.microsoft.com/en-us/powershell/scripting/dev-cross-plat/vscode/understanding-file-encoding)
GNU / Linux locale
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
整理 Windows 的字元編碼與換行符 - Shift_JIS / UTF-8 / UTF-16、亂碼、CRLF / LF,為何混亂
本文整理 Windows 上字元編碼與換行符容易混亂的核心:bytes、UTF-8 與 CP932、UTF-16LE、BOM、CRLF 與 LF 是不同軸的概念,亂碼源於以錯誤前提 decode,且誤儲存後無法還原。讀完即可在規格中寫出明確的 encoding 與換行約定,...
在 Windows 環境下減少 Codex 亂碼事故的最佳實務 - 先把『指示方式』釘住,再談環境整備
本文整理在 Windows 上讓 Codex 安全處理日文檔案的指示原則:讀檔前先確認 encoding 與 BOM,疑似亂碼禁止臆測儲存,既有檔案維持原狀,僅新建採 UTF-8,並在寫入後重新讀取代表性日文行驗證,把規則沉澱進 AGENTS.md 以減少事故。
ClickOnce 是什麼 - 以實務視角整理機制、更新、適合場面・不適合場面
本文以實務視角整理 ClickOnce 是什麼,從 manifest、快取、更新、簽章的構造,到適合公司內部 .NET 桌面業務應用程式的案件與不適合 machine-wide 或 service、driver 等深度 OS 整合的案件,幫助讀者判斷是否採用並掌握 depl...
用 Windows 沙箱加速 Windows 應用程式開發的驗證 - 以實務向整理管理員權限問題、乾淨環境、權限不足・資源不足的重現
整理在 Windows 應用程式開發中如何運用 Windows Sandbox 加速驗證的實務做法。透過按情境分檔的 .wsb、唯讀輸入與專用 Outbox 寫入分離、在容器內另建標準使用者重現權限不足、以及降低記憶體和關閉 vGPU 製造資源不足偏向,把每次的乾淨環境準備...
Windows 的 DLL 名稱解析機制 - 以實務角度整理搜尋順序、Known DLLs、API set、SxS
從實務角度整理 Windows 的 DLL 名稱解析,說明 loader 在掃描檔案系統前會先處理 DLL redirection、API set、SxS、Known DLLs,並用 SetDefaultDllDirectories 與 LoadLibraryEx 旗標縮小...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
作者檔案
本文作者的個人檔案頁面。
Go Komura
小村軟體有限公司 代表
以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。