1. 介绍
先说最重要的问题 — 为啥要折腾编码?看起来一个文件、一个编码,不就完事了。但实际情况比这复杂得多。你会很快发现文件可能来自任何地方,编码完全出乎意料。而你的应用当然期望的是另一种编码。
有时候你需要对接某个 API 或系统,它严格要求:只能用某种编码。也可能你翻到一个十年前的老文件,用新编辑器打开却看到乱码。或者你在保存 CSV 报表,希望确保同事们都能正常打开 — 无论是 Windows、Mac 还是 Excel。
总之,编码转换不是稀有或小众的事。相反,这是非常常见的问题:导出数据库、处理归档、对接财务系统或自动化脚本时都会遇到。越早搞清楚,后面惊喜越少。
从旧编码到新编码
要转换文件编码,需要两步:
- 读取 源文件,使用正确的源编码,得到字符串(或字符)。
- 写入 字符串到新文件,明确指定目标编码。
类比:把一本书从法语翻成俄语。先得会看法语(读出文本),然后把意思用俄语表达出来(写成俄语)。
在 C#(和 .NET)里,通过在 StreamReader(读)和 StreamWriter(写)的构造函数里传入相应的 Encoding 对象来实现。
编码概览与 Encoding 类
所有“魔法”字符转换都由 System.Text.Encoding 类完成。
下面表格列出常用编码以及在 C# 中如何获取它们:
| 编码 | 说明 | C# 常量 |
|---|---|---|
| UTF-8 | 通用,默认无 BOM | |
| UTF-8 带 BOM | 同上,但带 BOM 签名 | |
| UTF-16 (LE/BE) | “宽字符”,little-endian | Encoding.Unicode (LE), Encoding.BigEndianUnicode (BE) |
| ASCII | 7 位经典编码 | |
| Windows-1251 | 常用于西里尔字母(俄语) | |
| ISO-8859-1 | 拉丁字母(欧洲) | |
注意: 对于旧编码需要使用 Encoding.GetEncoding,有时用编号(比如 1251),有时用字符串(比如 "windows-1251")。
2. 转码:C# 中的逐步策略
来看看如何在实践中实现转码。
算法
- 用 StreamReader 打开源文件,明确指定源编码。
- 读取文本(全部或逐行 — 取决于文件大小)。
- 用 StreamWriter 打开目标文件,指定想要的目标编码。
- 把文本写入目标文件。
- 别忘了关闭流 — 用 using 来保证安全!
示例:从 Windows-1251 转到 UTF-8
假设有一个源文件 "input-1251.txt",它是 Windows-1251 编码,我们想要得到一个“纯净”的 UTF-8(无 BOM)的 "output-utf8.txt"。
// 指定带原始编码的源
using var reader = new StreamReader("input-1251.txt", Encoding.GetEncoding(1251));
using var writer = new StreamWriter("output-utf8.txt", false, Encoding.UTF8);
string line;
// 按行读取 — 对大文件更友好
while ((line = reader.ReadLine()) != null)
{
writer.WriteLine(line);
}
Console.WriteLine("文件已成功从 Windows-1251 转码为 UTF-8!");
这段代码保证每一行都能正确地从一种编码转换到另一种编码。
示意图大概是这样:
┌───────────────────────────────┐ ┌───────────────────┐ ┌───────────────────────────────┐
│ 文件 "input-1251.txt" (1251) │ --> │ StreamReader │ --> │ 内存中的 string 行 │
└───────────────────────────────┘ │ (Encoding 1251) │ └───────────────────────────────┘
└───────────────────┘
│
▼
┌────────────────────────┐
│ StreamWriter │
│ (Encoding UTF-8) │
└───────────┬────────────┘
│
▼
┌──────────────────────────────┐
│ "output-utf8.txt" (UTF-8) │
└──────────────────────────────┘
3. 实战示例:编码转换器
我们把情景复杂化一点:写一个简单的转换程序,允许用户自己选择源文件和目标文件,以及编码。
Console.WriteLine("请输入源文件路径:");
string inputPath = Console.ReadLine();
Console.WriteLine("源文件编码(例如:1251, utf-8):");
string sourceEncodingName = Console.ReadLine();
Console.WriteLine("请输入目标文件路径:");
string outputPath = Console.ReadLine();
Console.WriteLine("保存时使用的编码(例如:utf-8, 1251):");
string destEncodingName = Console.ReadLine();
Encoding sourceEncoding = Encoding.GetEncoding(sourceEncodingName);
Encoding destEncoding = Encoding.GetEncoding(destEncodingName);
using var reader = new StreamReader(inputPath, sourceEncoding);
using var writer = new StreamWriter(outputPath, false, destEncoding);
string line;
while ((line = reader.ReadLine()) != null)
{
writer.WriteLine(line);
}
Console.WriteLine("完成!请检查结果。");
建议: 如果不确定该填哪个编码,查文档或者试几个常见的。很多时候只有试验才能确认,尤其是没人留下 README 的情况下。
4. 重要细节:BOM、“不可见”字符和特殊情况
如何在保存时添加 UTF-8 的 BOM?
默认情况下,Encoding.UTF8 在 C# 中会创建 不带 BOM 的文件。如果需要带 BOM,可以用:
// true — 表示 "带 BOM"
var utf8WithBom = new UTF8Encoding(true);
using var writer = new StreamWriter("with-bom.txt", false, utf8WithBom);
writer.WriteLine("带 BOM 的文本");
文件现在会以三字节的“不可见”序列开头:0xEF, 0xBB, 0xBF。
什么时候 BOM 会惹麻烦?
- 如果你导出 CSV 给 Excel,大多数非本地化版本里 BOM 反而会导致怪字符出现。
- 在类 Unix 系统(Linux)下 BOM 有时会干扰文件处理。
- 对于 JSON 文件,BOM 几乎总是有害的,很多解析器不能正确处理!
建议: 有意识地选择是否需要 BOM,仅在必要时添加。
潜伏的不可见字符和魔法数字
如果打开“转码后”的文件时程序报格式错误,可能是你没考虑到文件开头的“不可见”字符(比如 BOM),或者源编码本身就选错了(比如把 1251 的文件当成 ASCII 读了)。务必确认应用/同事/服务器使用的到底是什么编码。
5. 实用小贴士
处理大文件:为啥按行读而不是一次性读入?
你也可以这样做:
string allText = File.ReadAllText("input.txt", Encoding.GetEncoding(1251));
File.WriteAllText("output.txt", allText, Encoding.UTF8);
这个方式适合小文件或中等大小文件(比如不超过 50MB)。但如果文件很大,整个文本会被加载到内存 —— 如果文件是几个 GB,后果自不待言。因此为通用性我们推荐按行读写。
在不太常见的编码之间转换
比如你突然收到了一个 ISO-8859-1 的文件(MySQL 导出时常见),想把它变成 Unicode:
var sourceEnc = Encoding.GetEncoding("iso-8859-1");
var destEnc = Encoding.UTF8;
using var reader = new StreamReader("data-latin.txt", sourceEnc);
using var writer = new StreamWriter("data-unicode.txt", false, destEnc);
string line;
while ((line = reader.ReadLine()) != null)
{
writer.WriteLine(line);
}
只要 .NET 支持该编码,这种方法对任何编码都有效。
不要对二进制文件做转码!
上面描述的程序仅适用于 文本文件。如果文件是二进制(比如图片、音频、归档),尝试把它当文本读写会破坏字节流,导致乱码或文件损坏。二进制文件没有“编码”的概念,不能这么转换。
对应表:哪些编码适合什么场景
| 编码 | 何时使用 |
|---|---|
| UTF-8 | 通用文件、Web、现代应用 |
| UTF-8 带 BOM | 为了和 Notepad、Excel 兼容 |
| Windows-1251 | “老派”应用、本地俄语程序 |
| ASCII | 只有英文内容的文件 |
| UTF-16 | 特殊情况,某些非主流应用 |
辅助技巧和建议
怎么判断文件编码?
- 专用编辑器(Notepad++、Visual Studio Code)通常会识别并显示编码。
- 如果没有 BOM,但看起来内容“正确”而字母不对 —— 很可能编码不匹配。
能自动识别编码吗?
- .NET 没有“万能钥匙”能 100% 判断任意文件编码。一般规则是:有 BOM 就能确定是带 BOM 的 UTF,没 BOM 时只能根据内容猜测或试错。
如果部分行看起来正常而部分出现乱码怎么办?
- 可能文件是混合的或被损坏。检查是否有多个程序以不同编码写入过同一个文件。
6. 常见错误及如何避免
为了更好理解,来看常见错误:
源编码弄错。 如果你以为文件是 UTF-8,但其实是 Windows-1251 —— 会得到一堆“楼梯”而不是西里尔字母。确认源编码,若不确定,用 Notepad++ 等工具查看。
BOM 放错地方。 有时加上 BOM 会破坏解析,有时不加 BOM 又会导致对方误判编码。
一次性把整个文件读入内存。 文件太大时请改用按行读取,避免程序把所有 RAM 都吃光。
写文件时不指定编码。 StreamWriter 默认使用 UTF-8 不带 BOM。如果你需要别的,请显式传入编码。
错误使用 GetEncoding。 如果字符串写错了,比如写成 "utf8" 而不是 "utf-8",会抛异常。使用正确的名字或编号(例如 1251)。
GO TO FULL VERSION