理清 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 点:

  • 乱码不是「文字」的问题,而是「字节序列被如何解释」的问题
  • 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 与预期的 encoding

2. 乱码的本质

乱码的本质相当单纯。

  1. 把字符串以某种文字编码 encode 成字节序列
  2. 把该字节序列以某种文字编码 decode 还原成字符串
  3. 如果 encode 与 decode 的前提不一致,就会被读成另一个字符串

例如,把 以 UTF-8 保存,字节序列会是:

E3 81 82

如果把这个字节序列当作 UTF-8 读取,就是 ,但如果在 CP932 的语境下读取,就会看成 縺� 这样完全不同的字符串。这就是乱码。

重要的是,这里发生的并不是「日文本身坏了」,而只是 对同一组 bytes 的解释产生了偏差

2.1 只是显示错乱的话,有时还能挽回

乱码中存在还能挽回的阶段。例如,只要 原本的 bytes 没有改变,用正确的 encoding 重新打开,有时是可以还原的。

反过来危险的是下面这样的流程:

  1. 把 UTF-8 的文件误读为 CP932
  2. 画面上看起来像 縺�
  3. 就这样把「看到的字符串」保存下来
  4. 原本的 UTF-8 bytes 就丢失了

一旦进入这个阶段,就不再是单纯的显示错乱,而是 数据损坏

2.2 更危险的是把「无法表示的字符」写入狭窄的 code page 时

另一个典型事故,是把 Unicode 字符串写入 CP932 这类 legacy code page 的时候。

例如,如果包含了对方 code page 中不存在的字符,就会发生:

  • 被替换成 ?
  • 混入替换字符
  • 被转换成相近的其他字符
  • 转换失败

这类情况。

这个事故不应只看 能不能读,而应该看 往返转换后能否还原。一旦丢失的字符,之后即便知道了正确的 encoding,也无法复原。

3. 为什么 Windows 上容易变得复杂

Windows 之所以复杂,并不只是因为它历史悠久。而是因为 Unicode 的世界和 legacy code page 的世界,至今仍然共存

3.1 Windows API 中 Unicode 系与 code page 系并存

Windows API 大致分为两大系统:

  • W 系:wide character,用 UTF-16 处理 Unicode 的系列
  • A 系:被称为 ANSI 的 code page 系列

也就是说,在 Windows 内部,从一开始就同时存在「用 Unicode 处理」和「用当时的 active code page 处理」这两条路。因此,即使在同一台 Windows 机器上,也会因为 经过了哪个 API 或哪个工具 而使前提发生变化。

3.2 「Windows 的日文」并非只有一种

在 Windows 的日文相关问题上,实务中经常混杂的是以下这 4 种:

  • CP932:常见于日文 Windows 的 legacy text
  • UTF-8:在新的文本资产、web、跨平台系统中越来越多
  • UTF-16LE:在 Windows 系工具或 API 的语境中,现在依然普遍出现
  • console 的 code page:影响 cmd.exe 和部分 console 工具输入输出的另一层

这里重要的是,并不是执行了 chcp 65001 之后文件就变成 UTF-8 了。改变 console 的 code page 和现有文件的 bytes 是什么,是两个不同的问题。

另外,虽然常常把日文 Windows 的 legacy text 笼统地称为「Shift_JIS」,但在实务中,用 CP932 这个名称来意识它,交流起来会更不容易出偏差。至少能明确表示「现在讨论的是源自 Windows 的日文 legacy encoding」。

3.3 文件名和文件内容是两个不同的问题

在 Windows 上,如果日文文件名显示得很正常,就容易让人觉得「那内容应该也没问题吧」。这一点很危险。

  • 处理 path / 文件名的层
  • 读取文件内容的层
  • 在 console 中显示的层

这 3 者是不同的。

举例来说,即使日文 path 能够正常处理,但如果文件内容以 CP932 保存,而在 Linux 侧被当作 UTF-8 读取,内容仍然会损坏。反过来,即使文件内容是 UTF-8,如果 console 的 code page 不匹配,也只会导致显示错乱。

3.4 PowerShell 及周边工具的默认值也并不统一

在 Windows 上悄悄增加事故的一个原因是,即使自认为写的是同一份「文本」,输出的 bytes 也会因路径不同而不同

特别要注意的是这些:

  • Windows PowerShell 5.1 的默认 encoding 并不统一
  • 部分 cmdlet 或 redirection 会生成 UTF-16LE
  • 另一些路径则使用 active ANSI code page
  • PowerShell 7 及以后 默认是 UTF-8 no BOM

也就是说,仅凭「用 PowerShell 输出的文本」这一点,是无法确定 encoding 的。必须看清楚是哪个版本、哪个 cmdlet、走了哪条写入路径

4. 与 Linux 组合使用时的典型事故

单独在 Windows 上运转还算凑合的东西,一旦夹入 Linux 就突然坏掉,这并不罕见。理由很简单,因为 Linux 一侧 以 UTF-8 为前提的情况居多

4.1 Windows 以 CP932 保存的文本被 Linux 当作 UTF-8 读取

这是最常见的事故。

  • Windows 的 legacy app 或旧有运维流程以 CP932 写入 CSV / TXT / log
  • Linux 一侧的脚本或工具按照 locale,以 UTF-8 为前提读取
  • 结果出现 decode error、、莫名其妙的字符串

这时并不是 Linux 一侧的工具有问题,根本原因在于 接收到的 bytes 没有附带 encoding 的约定

4.2 Linux / VS Code 制作的 UTF-8 no BOM 被 Windows 一侧当作 ANSI

反方向的事故也存在。

  • 在 Linux 或 VS Code 上制作 UTF-8 no BOM 的脚本 / 配置 / 文本
  • Windows PowerShell 5.1 或 legacy 工具把没有 BOM 的文件当作 ANSI 侧的 code page
  • 只有包含日文或 non-ASCII 的行会损坏

这里容易被当成「罪魁祸首」的是 UTF-8,但实际原因是 混入了无法正确推断无 BOM UTF-8 的读取方

4.3 Windows 一侧写入 UTF-16LE,Linux 一侧「看起来不像文本」

这种情况也相当常见。

  • Windows PowerShell 5.1 的部分输出或 legacy 工具写入 UTF-16LE
  • Linux 一侧的文本工具假设的是 UTF-8 的单字节 stream
  • 结果变成混杂大量 NUL byte 的「类似二进制的文本」

UTF-16LE 本身并没有问题。只是很多场合下,它与直接流入 Linux 文本处理工具的前提并不吻合

4.4 是否带 BOM 也会引起摩擦

BOM 本身并不是 encoding,但在实务中影响相当大。

  • Windows 一侧的部分工具,有 BOM 会更省心
  • Linux 一侧的部分工具,会把 BOM 当作开头多出来的字节
  • 结果就是第一列或第一行开头损坏、混入看不见的垃圾字符、比较结果产生偏差

尤其在 UTF-8 中,即使同样是 UTF-8,有无 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 能读,所以文件也没问题」或「因为 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,行为类似二进制
带 BOM 的 UTF-8 文件 UTF-8 + BOM Unix 系工具假设为纯 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 的脚本吗
  • 是 VS Code 吗
  • 是来自 Excel 的 export 吗
  • 是某种 middleware / batch / CI 吗

如果这里含糊不清,encoding 的推断就会变成靠运气。

5.3 现在是谁、以什么前提读取的

除了书写者之外,读取者的前提也需要确认。

  • 编辑器是否在自动检测(auto-detect)
  • PowerShell 是否在看 BOM
  • Linux 一侧是否按照 locale 当作 UTF-8 处理
  • library 是否使用了默认 encoding
  • 是否明确指定了 Encoding.UTF8cp932

乱码几乎都在这一环节发生。

5.4 读错的内容是否已经被保存

最后,要确认 损害是否止步于显示层面

  • bytes 是否仍然保持原样
  • 是否有人把看起来已经损坏的内容保存了下来
  • 差分中是否混入了 ?
  • 全文是否已经被用另一种 encoding 重写

只要把这 4 个问题都填满,大多数情况下原因就能看清楚。

6. 减少事故的运维规则

接下来是偏实务的内容。在跨越 Windows 和 Linux 的项目中,如果一开始就先定好下面这些规则,事故会明显减少。

6.1 新建文件以 UTF-8 为第一候选

新建的文本文件,先以 UTF-8 为第一候选是比较稳妥的做法。但不能止步于此,还需要连 BOM 怎么处理一起决定好

推荐的决定方式是:

  • 主要由 Linux 一侧读取的文本:以 UTF-8 no BOM 为基本原则
  • Windows 的 legacy 工具或 Windows PowerShell 5.1 会读取的脚本:按对方的需要明确指定是否带 BOM
  • 确实有明确需要 UTF-16LE 的对象时,就把这个要求写进规格说明里

只写「统一成 UTF-8」,之后一定会在 BOM 上产生争议。

6.2 既有的 legacy 文件维持现状,直到有明确的迁移任务

如果既有文件是 CP932,不要在日常功能修改时顺手转换成 UTF-8,这样更安全。

运维上比较安全的形式是:

  • 既有文件维持 原本的 encoding / BOM / 换行符
  • encoding 的变更,作为 独立的迁移任务 分离出来
  • 先确认转换对象、影响范围、下游 consumer,再进行统一转换

许多乱码事故,都是从善意的「顺手统一成 UTF-8」开始的。

6.3 把 encoding 当作 interface 的一部分来对待

CSV、TXT、log、配置文件、简易 protocol,不仅内容本身,encoding 本身就是一种 interface

例如,至少应该把下面这些写进规格说明里:

  • 这个文件是 UTF-8 / CP932 / UTF-16LE 中的哪一种
  • 如果是 UTF-8,是否带 BOM
  • 换行符是 LF 还是 CRLF
  • Linux / Windows 哪一方是 producer / consumer
  • 中途的 batch 或 ETL 是否会重新保存

「用 text 传递」本身不能算作规格。

6.4 不要相信默认值,写入时要明确指定

无论是在代码里还是脚本里,明确指定 encoding 都会更安全。

危险的想法是这样的:

  • 保持默认设置直接保存
  • 觉得配合 OS 大概就没问题了
  • 觉得 console 能读,文件应该也没问题
  • 觉得有 auto-detect 所以没问题

默认值会因 Windows / Linux、PowerShell 5.1 / 7、编辑器、runtime 的不同而普遍发生变化。除非明确指定,否则很容易只是「碰巧能用」而已

6.5 把 console 和文件分开确认

一个不起眼但相当有效的规则是这个。

  • 在 console 上的显示确认
  • 重新打开文件后的确认

把这两者分开。

即使 chcp 或 terminal 的显示是对的,如果保存下来的文件是另一种 encoding,那也没有意义。反过来,即使文件本身正常,如果 console 显示用的 code page 不匹配,也只会导致外观上的错乱。

6.6 Git 不会替你修正 encoding

这一点不起眼,但很重要。

Git 基本上只会 追踪 bytes。也就是说,即使是损坏的 bytes,它也会原样认真地记入历史。

因此,当出现:

  • 明明什么都没改却出现巨大差分
  • 只有日文行出现莫名其妙的差分
  • 只有开头行发生变化
  • 换行符和 encoding 同时发生变化

这类情况时,应该先怀疑 re-encoding 事故,而不是先怀疑内容本身发生了变化。

7. 最低限度的检查清单

在混合 Windows 与 Linux 的项目中,先把想固定下来的检查清单放在这里。

7.1 编辑前

  • 这个文件目前的 encoding 是什么
  • 是否带 BOM
  • 换行符是 LF 还是 CRLF
  • 是否已记录 2〜3 行有代表性的日文内容
  • 是否清楚最终 consumer 是 Linux 一侧还是 Windows 一侧

7.2 编辑中

  • 是否依赖默认 encoding 进行了写入
  • 是否把 save 交给了 auto-detect
  • 是否随意使用了 PowerShell 或 shell redirection 的路径
  • 是否仅凭「显示能读」就放心了

7.3 编辑后

  • 保存后是否重新打开确认过
  • Linux 一侧和 Windows 一侧的代表行是否都没有损坏
  • 差分中 ? 是否有增加
  • 是否只有开头行或开头列损坏
  • 是否变成了只有 BOM / 换行符的巨大差分

7.4 应该作为迁移任务处理的事项

  • CP932 → UTF-8 的批量转换
  • 统一 UTF-8 BOM policy
  • 盘点以 PowerShell 5.1 为前提的脚本
  • 明文化经过 CI / container / WSL / SSH 的文本传递路径
  • 统一编辑器 / formatter / batch 的保存设置

8. 总结

如果用一句话概括 Windows 的文字编码问题,本质就是 Unicode 的世界和 legacy code page 的世界,至今仍然共存

而与 Linux 搭配时事故增多的原因,是因为 Linux 一侧大多以 UTF-8 为前提运转,于是 Windows 一侧的 CP932、UTF-16、console code page、PowerShell 版本差异,就会一次性浮现出来。

想要记住的是下面这 5 点:

  • 乱码是 bytes 解释上的偏差
  • 显示错乱和数据损坏是两件不同的事
  • 在 Windows 上,要把文件 / 编辑器 / console / API 这几层分开来考虑
  • 与 Linux 交互的文本以 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 上发生乱码的原因是什么?
大多数原因是把同一组 bytes 当成了另一种文字编码来读取,或者把误读后的结果又用另一种编码保存了回去。例如「あ」以 UTF-8 保存的字节序列(E3 81 82),如果在 CP932 的语境下读取,就会看成「縺」这样完全不同的字符串。这并不是因为日文难处理才发生的,其本质是 encode 和 decode 的前提不一致。
乱码的文件还能恢复吗?
只要原本的字节序列没有被改变,用正确的 encoding 重新打开有时是可以恢复的。危险的是把误读后看起来已经损坏的内容直接保存回去,一旦进入这个阶段,就不再是显示错乱,而是数据损坏了。另外,如果把 Unicode 字符串写入 CP932 这类狭窄的 code page,变成「?」或替换字符,那些一旦丢失的字符,即使之后知道了正确的 encoding 也无法恢复。
执行 chcp 65001 之后文件就会变成 UTF-8 吗?
不会。改变 console 的 code page,和现有文件的字节序列本身是什么,是两个不同的问题。在 Windows 上,文件本身的文字编码、编辑器的解读、console 的 input/output code page、应用内部的字符串格式是各自独立的层,需要分开来考虑。「因为 console 能读,所以文件也没问题」这种判断很容易出错,因此应该把 console 显示的确认,和重新打开文件的确认分开进行。
在 Windows 和 Linux 之间交换文本时,有哪些安全的规则?
新建文件以 UTF-8 为第一候选,并要连 BOM 的处理方式一起决定好。主要由 Linux 侧读取的文本,以 UTF-8 no BOM 为基本原则;如果是 Windows PowerShell 5.1 等 legacy 工具会读取的情况,就按对方的需要明确指定是否带 BOM。现有的 CP932 文件不要在日常改修时顺手转换,而应该作为明确的迁移任务单独处理。另外,CSV 和日志的 encoding 本身就是一种 interface,把 encoding、BOM、换行符写进规格说明里,能减少事故。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表