正在加载,请稍候…

从乱码到 UTF-8:字符编码完全指南

通过实际例子理解 ASCII、GBK、Unicode 和 UTF-8。了解乱码产生的原因以及如何在项目中避免编码问题。

你可能在旧文件或某些网站上见过像 锟斤拷ä½ å¥½ 这样的乱码。这就是 mojibake ——字符编码不匹配的结果。理解计算机如何存储和解释文本对每个开发者都至关重要。本指南涵盖从 ASCII 到 UTF-8 的演变、常见陷阱以及一个实际示例,帮助你掌握编码知识。

带有发光指示灯的服务器机架

什么是字符编码?

计算机只理解二进制——0 和 1。要显示字母 A 或汉字 ,机器需要在人类可读的符号和二进制序列之间建立映射。这种映射就是 字符编码

字符集 为每个字符分配一个唯一的数字(码点)。例如,在 Unicode 中,A 是 U+0041, 是 U+4E2D。编码 随后将该码点转换为特定的字节序列。UTF-8、UTF-16 和 GBK 都是实现不同字符集的编码。

编码简史

ASCII:美国标准

ASCII(美国信息交换标准代码)于 1963 年推出。它使用 7 位表示 128 个字符:英文字母、数字、标点符号和控制码。每个值低于 128 的字节都是 ASCII 字符。

范围(十进制) 类别 示例
0–31 控制字符 空字符(0),换行符(10)
32 空格
48–57 数字 09
65–90 大写字母 AZ
97–122 小写字母 az

关键事实:'A' = 65,'a' = 97。差值为 32 —— 对大小写转换很有用。

GBK:中文字符编码

ASCII 无法表示汉字。20 世纪 80 年代,中国开发了 GB2312(每个汉字 2 字节,6763 个字符)。后来,GBK(国标扩展)将其扩展到 21003 个字符,包括繁体中文。GBK 多年来一直是中文 Windows 系统上的默认 ANSI 编码。

Unicode:一统江湖

Unicode 为每种书写系统中的每个字符提供了唯一的码点——目前超过 14 万个字符。编码空间范围从 U+0000 到 U+10FFFF(1,114,112 个可能的码点)。Unicode 本身只是一个字符集;它没有规定如何将码点存储为字节。

UTF-8:主流编码

UTF-8 是 Unicode 的一种变长编码。每个码点使用 1 到 4 个字节:

码点范围 UTF-8 字节
U+0000–U+007F 1 字节(0xxxxxxx)
U+0080–U+07FF 2 字节(110xxxxx 10xxxxxx)
U+0800–U+FFFF 3 字节(1110xxxx 10xxxxxx 10xxxxxx)
U+10000–U+10FFFF 4 字节(11110xxx 10xxxxxx 10xxxxxx 10xxxxxx)

UTF-8 向后兼容 ASCII:任何 ASCII 文本都是有效的 UTF-8。它对拉丁文字节省空间,并且具有自同步能力(丢失一个字节只会损坏当前码点)。如今,超过 98% 的网页使用 UTF-8。

常见陷阱

  • 乱码:以 UTF-8 打开 GBK 编码的文件会产生像 锟斤拷 这样的乱码。解决方法:始终知道原始编码并使用它解码。
  • BOM(字节顺序标记):某些 UTF-8 文件以 \xEF\xBB\xBF 开头。这个 BOM 会混淆解析器(例如 JSON)。首选无 BOM 的 UTF-8。
  • 数据库编码:MySQL 的 utf8 字符集只支持最多 3 字节的 UTF-8,缺少 emoji。请改用 utf8mb4
  • 字符串长度:JavaScript 中的 String.length 计算的是 UTF-16 代码单元,而不是码点或字素簇。像 😀 这样的 emoji 是 2 个代码单元(代理对)。对于面向用户的长度,请使用 Intl.Segmenter 或库。
  • 规范化:Unicode 允许某些字符有多个表示形式(例如 é 可以是 U+00E9 或 U+0065 U+0301)。在比较字符串之前先进行规范化(NFC)。

实际示例:解码乱码文件

假设你有一个用 GBK 编码保存的文件 data.txt,但你在 UTF-8 终端中打开它。你会看到:

ä½ å¥½ ä¸ç

这是 你好世界 的 GBK 字节被解释为 UTF-8 的结果。让我们逐步修复:

  1. 将文件作为字节读取。
  2. 使用 GBK 解码这些字节。
  3. 将结果重新编码为 UTF-8 以供现代使用。

在 Python 中:

# 从文件中读取原始字节
with open('data.txt', 'rb') as f:
    raw_bytes = f.read()

# 使用原始编码(GBK)解码
text = raw_bytes.decode('gbk')
print(text)  # 输出:你好世界

# 重新编码为 UTF-8 以供现代存储
utf8_bytes = text.encode('utf-8')
with open('data_utf8.txt', 'wb') as f:
    f.write(utf8_bytes)

如果你不知道原始编码,可以尝试常见的编码(GBK、Shift-JIS、ISO-8859-1)并检查输出。像 chardet 这样的工具可以帮助猜测。

何时使用哪种编码?

场景 推荐编码
网页、API、JSON UTF-8(无 BOM)
遗留中文文本文件 GBK(如果原始)
Windows 系统 API(历史) UTF-16
内部处理 UTF-8 或 UTF-32
数据库 utf8mb4(MySQL),NVARCHAR(SQL Server)

常见问题

Unicode 和 UTF-8 有什么区别?

Unicode 是一个 字符集,为每个字符分配一个唯一的数字(码点)。UTF-8 是一种 编码,将这些数字序列化为字节。Unicode 定义了“是什么”,UTF-8 定义了“如何存储”。

为什么我会看到 锟斤拷

这是一个经典的乱码链:以 GBK 编码的文件被解码为 UTF-8,产生了替换字符(\uFFFD),然后这些字符被重新编码并再次以 GBK 解码。结果是重复的字符 锟(0xEFBFBD)斤(0xEFBFBD)

如何将文件从 GBK 转换为 UTF-8?

在 Linux/macOS 上使用 iconv -f gbk -t utf-8 input.txt > output.txt,或使用上面实际示例中的 Python 方法。

我应该使用带 BOM 还是不带 BOM 的 UTF-8?

在大多数情况下(网页、JSON、Unix)使用不带 BOM 的 UTF-8。如果需要在遗留 Windows 工具(如记事本)中区分 UTF-8 和其他编码,则使用带 BOM 的 UTF-8。

MySQL 中的 utf8utf8mb4 有什么区别?

MySQL 中的 utf8utf8mb3 的别名,只支持最多 3 字节的 UTF-8 字符(BMP)。它无法存储 emoji 或 U+FFFF 以上的字符。utf8mb4 支持完整的 4 字节 UTF-8 范围。

总结

  • 字符编码将字符映射到字节。编码不匹配会导致乱码。
  • ASCII 是基础(7 位,128 个字符)。
  • GBK 处理中文文本,但不通用。
  • Unicode 提供了通用字符集;UTF-8 是主流编码。
  • 始终知道你的编码,首选无 BOM 的 UTF-8,并在 MySQL 中使用 utf8mb4
  • 要探索 Unicode 编码的实践工具,请尝试我们的 文本转 Unicode 转换器