跳转到主要内容

developer-tools

Unicode、UTF-8与字符编码:每位开发者都应该了解的知识

robinhood-projects3 分钟阅读

Base64 编解码工具

在浏览器中即时将文本或二进制数据编码为 Base64,或将 Base64 字符串解码为纯文本。

使用工具 →

JSON 查看器、格式化工具与编辑器

在实时交互树中查看、编辑和格式化 JSON。支持内联值编辑、键名重命名、添加/删除节点、JSONC 注释以及一键导出——全部在浏览器中完成。

使用工具 →

URL 编码解码工具

对 URL 进行百分比编码以安全用于查询字符串,或将已编码的 URL 解码为纯文本。

使用工具 →

Unicode、UTF-8与字符编码:每位开发者都应该了解的知识

您从网页表单复制一个名字,结果它以é的形式保存到数据库中。您在Excel中打开CSV文件,发现所有带重音的字符都显示错误。用户在文本字段中粘贴了一个表情符号,整个MySQL插入操作就以一个神秘的错误失败了。您在文件中添加了BOM让Excel能正确读取,结果shell脚本在第一行就崩溃了。

这些都是字符编码错误。它们是开发者可能遇到的最令人迷惑的问题之一,因为症状看起来是随机的——同一个字符串在一种情况下能正确显示,在另一种情况下却变成乱码。原因几乎总是相同的:两个软件对字节如何映射到字符存在分歧。

本指南从基础开始解释这种映射:什么是字符编码、Unicode和UTF-8如何工作,以及如何诊断和修复现实中最常见的错误。

Unicode前的时代:ASCII与扩展ASCII的混乱

计算机中存储的每个字符本质上都是一个数字。字符编码是将数字映射到字符及其逆过程的查找表。

第一个被广泛采用的编码是ASCII(美国信息交换标准代码),发布于1963年。ASCII使用7位,共有128个槽位(0–127),涵盖英文大小写字母、十个数字、常用标点和33个不可打印的控制字符(如换行和制表符)。

128个字符对英语来说已经够用,但对法语、德语、西班牙语、阿拉伯语、中文、日语或世界上其他数千种书写语言来说远远不够。

扩展ASCII的问题

字节中的第8位是可用的,这意味着还有128个额外的槽位(128–255)。不同的组织和操作系统争相以不同方式填充这些槽位:

  • Windows-1252(也称CP1252):Microsoft Windows在西欧使用,将é、ñ和€等字符放在上半部分。
  • ISO-8859-1(Latin-1):西欧语言的国际标准,与Windows-1252有大量重叠但并不完全相同——128–159范围在Latin-1中是控制字符,但在Windows-1252中是可打印字符。
  • ISO-8859-5:涵盖西里尔字母。
  • ISO-8859-6:涵盖阿拉伯字母。
  • Shift JIS、EUC-JP、Big5:东亚语言使用的完全不同的多字节方案。

结果是,在一个系统上创建的文件在另一个系统上显示为乱码,除非两者使用相同的编码——而且没有可靠的方式来表明文件使用了哪种编码。软件只能猜测,而且不断猜错。

这就是创建Unicode要解决的问题。

Unicode:面向所有语言的统一字符集

Unicode不是一种编码,而是一个字符集——一个庞大的查找表,为每种使用中的书写系统中的每个字符分配一个唯一的数字,加上数千个符号、表情符号和历史文字。

该表中的每个条目称为码点,写作U+后跟一个十六进制数。几个例子:

字符码点描述
AU+0041拉丁大写字母A
éU+00E9带尖音符的拉丁小写字母e
中U+4E2DCJK统一表意文字(zhōng)
😀U+1F600咧嘴笑脸表情符号
𝄞U+1D11E音乐符号高音谱号

Unicode标准目前定义了超过140,000个字符,并定期更新。

Unicode平面

Unicode码点从U+0000到U+10FFFF,提供略超过110万个可能的槽位。该空间被划分为17个平面,每个平面包含65,536个码点。

  • 平面0——基本多文种平面(BMP): U+0000到U+FFFF,包含当今常用的几乎所有字符:拉丁文、希腊文、西里尔文、阿拉伯文、希伯来文、CJK等。
  • 平面1——多文种补充平面(SMP): U+10000到U+1FFFF,包含表情符号、历史文字、音乐符号。
  • 平面2–16: 其他CJK统一表意文字、罕见历史文字和未分配空间。

破坏MySQL表的表情符号存在于平面1中,这个细节很重要,我们很快就会看到。

UTF-8:Unicode实际上如何存储

Unicode定义了每个字符获得哪个数字,但不指定如何将这些数字作为字节存储在磁盘或内存中,那是Unicode编码的工作。

UTF-8是目前网络和现代软件中使用最广泛的Unicode编码,它是可变宽度的,这意味着不同字符占用不同数量的字节:

码点范围使用字节数谁住在这里
U+0000 – U+007F1字节ASCII(所有128个字符)
U+0080 – U+07FF2字节扩展拉丁文、希腊文、西里尔文、阿拉伯文、希伯来文
U+0800 – U+FFFF3字节大多数CJK字符,BMP其余部分
U+10000 – U+10FFFF4字节表情符号、稀有文字

UTF-8字节序列的工作原理

UTF-8是自同步的。任何字符的第一个字节告诉您后面有多少字节:

  • 0xxxxxxx — 1字节字符(ASCII)。任何有效的ASCII文件都是有效的UTF-8。
  • 110xxxxx 10xxxxxx — 2字节字符。
  • 1110xxxx 10xxxxxx 10xxxxxx — 3字节字符。
  • 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx — 4字节字符。

延续字节始终以10开头,因此您可以立即判断任何给定字节是新字符的开始还是某个字符的中间部分。这使得在读取部分流后进行重新同步成为可能。

具体示例——编码é(U+00E9):

U+00E9的二进制是11101001,落在2字节范围内。模板110xxxxx 10xxxxxx产生11000011 10101001——字节0xC3 0xA9——这正是UTF-8为é存储的内容。

UTF-8为什么占据主导地位

UTF-8有三个使其胜出的属性:

  1. ASCII兼容性。 任何纯ASCII文件都是有效的UTF-8,迁移现有系统不需要修改没有非ASCII内容的文件。
  2. 拉丁文字的空间效率。 英文文本每个字符使用一个字节,与ASCII相同,只有ASCII以外的字符才需要更多字节。
  3. 无字节顺序歧义。 字节结构无需任何头部就是明确的。(与UTF-16相比,UTF-16需要字节顺序标记。)

UTF-16和UTF-32:何时会遇到它们

UTF-8不是唯一的Unicode编码,某些环境使用其他编码。

UTF-16将BMP字符编码为两个字节(一个16位代码单元),将补充平面字符编码为四个字节(两个16位代码单元,称为代理对)。Windows内部、Java字符串、JavaScript字符串(在引擎层面)和.NET字符串都在内部使用UTF-16。

UTF-32每个字符始终使用四个字节。它浪费空间,但使按字符索引的随机访问非常快速。某些数据库内部使用它。

字节顺序标记(BOM)

UTF-16有字节顺序问题。两字节序列可以以大端序(A为0x00 0x41)或小端序(0x41 0x00)存储。接收方需要知道使用了哪种顺序。

解决方案是字节顺序标记:码点U+FEFF放在文件最开头。在UTF-16大端序中,显示为字节FE FF;在UTF-16小端序中,显示为FF FE。看到其中之一的读取器就知道了字节顺序。

UTF-8技术上也有BOM(EF BB BF),但对UTF-8来说它毫无意义,因为UTF-8没有字节顺序歧义。这就是它造成问题的地方:

  • Excel和CSV文件。 Excel使用UTF-8 BOM作为信号来将CSV解释为UTF-8。没有它,Excel默认使用系统区域设置编码(通常是Windows-1252),导致带重音字符显示错误。许多开发者专门添加BOM来让Excel正常工作。
  • Shell脚本。 以#!/bin/bash开头的shell脚本必须将该序列作为最开始的字节。如果UTF-8 BOM在shebang前面,内核看到EF BB BF 23 21而无法识别shebang,导致脚本失败或使用错误的解释器运行。

经验法则:为面向Excel的CSV文件添加BOM,永远不要将其添加到shell脚本或JSON文件中。

现实中常见的编码错误

乱码:将UTF-8读取为Latin-1

乱码(Mojibake,文字化け)是日语中因编码不匹配导致文字混乱的术语。现代网络中最常见的变体:UTF-8字符串被当作Latin-1(ISO-8859-1或Windows-1252)读取。

字符é是U+00E9,在UTF-8中存储为两个字节0xC3 0xA9。如果程序将这两个字节读取为Latin-1,它看到两个单独的字符:0xC3是Ã,0xA9是©。字符串café变成了café。

这种特定的损坏模式——é变为é、è变为è、à变为à ——是UTF-8被解释为Latin-1的明确标志。您最常在以下情况中看到它:

  • 插入时连接字符集未设置为UTF-8的数据库。
  • 通过较旧邮件服务器发送的电子邮件。
  • 在区域设置不匹配的系统之间复制的文件。

解决方法是找出不匹配发生的地方,并在该层纠正编码声明——而不是事后修补损坏的字符串。

替换字符(U+FFFD)

当解码器遇到在预期编码中无效的字节序列时,它必须做些什么。Unicode标准建议替换为替换字符U+FFFD,显示为<0xFFFD>。如果在输出中看到?方框或<0xFFFD>字符,说明解码器遇到了无法解释的字节。

常见原因:

  • 截断多字节UTF-8序列。如果在落在3字节CJK字符内部的字节边界处切割字符串,部分序列是无效的。
  • 将Latin-1编码的文件传递给严格的UTF-8解码器。
  • 从混合编码的来源接收数据。

MySQL的utf8与utf8mb4:表情符号问题

这是网络开发中最著名的编码错误之一。MySQL有两个字符集,分别叫做utf8和utf8mb4。

MySQL的utf8不是真正的UTF-8。它仅支持U+FFFF以内的码点——恰好是基本多文种平面——每个字符最多使用三个字节。表情符号位于平面1(U+10000及以上),需要四个字节。当您尝试将四字节UTF-8字符插入定义为utf8的列时,MySQL会静默截断值或抛出错误。

utf8mb4是真正的UTF-8,支持完整的四字节。它专门是为了处理表情符号和其他补充平面字符而引入的。

正确的做法:对新表和数据库始终使用utf8mb4,并且显式设置连接字符集和排序规则:

ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE posts CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

并在应用程序配置中设置连接参数:

charset=utf8mb4

在MySQL中使用utf8是一个遗留陷阱,应将其视为已弃用。

Python中的字符串长度与字符计数

Python 2默认将字符串存储为字节序列。对于UTF-8字符串,len("café")返回5——因为é占用两个字节——而不是4。正确处理多字节需要显式的u""unicode字面量和仔细的编码/解码规范。

Python 3修复了这个问题:str是Unicode码点的序列,所以len("café")返回4。字节数据移到了bytes类型,通过显式的.encode()和.decode()调用处理。

但还有一个细微之处。Python 3的len()计算码点,而不是字素簇。字符é可以表示为单个码点(U+00E9),也可以表示为e(U+0065)加上一个组合尖音符(U+0301)。两者渲染完全相同,但产生不同的len()值。向用户展示字符计数的多语言应用程序应该使用Unicode分段库,而不是原始的len()。

当您检查包含多语言字符串的JSON数据时,可能会注意到这一点——看起来像单个字符的内容在内部可能是两个码点。

编码对比表

编码最大字符数ASCII字符字节数表情符号字节数常见上下文
ASCII1281N/A传统系统、协议头
Latin-1 (ISO-8859-1)2561N/A西欧传统文本
Windows-12522561N/A传统Windows文档
UTF-81,114,11214Web、Linux、macOS、现代API
UTF-161,114,11224Windows、Java、.NET、JavaScript引擎内部
UTF-321,114,11244某些数据库内部、文本处理
MySQL utf8~65,5361损坏旧版MySQL列——避免使用
MySQL utf8mb41,114,11214MySQL——使用这个

如何检测和修复编码问题

检测

在Python中:

import chardet
 
with open("mystery_file.txt", "rb") as f:
    raw = f.read()
 
result = chardet.detect(raw)
print(result)  # {'encoding': 'Windows-1252', 'confidence': 0.73, 'language': ''}

chardet库使用统计分析从字节模式猜测编码。置信度分数是诚实的——编码检测是启发式的,不是确定性的。

在浏览器中: 打开DevTools,进入网络选项卡,找到HTML页面的响应,检查Content-Type头。它应该显示text/html; charset=utf-8。如果缺失或不正确,浏览器将猜测,通常是错误的。

在数据库中: 在数据库、表和列级别检查字符集:

SHOW VARIABLES LIKE 'character_set%';
SHOW CREATE TABLE posts;

修复乱码数据库

如果数据以UTF-8插入但以Latin-1存储(常见的迁移错误),有时可以恢复:

-- 将损坏的数据作为二进制读取,然后解释为UTF-8
UPDATE posts
SET body = CONVERT(CAST(CONVERT(body USING latin1) AS BINARY) USING utf8mb4)
WHERE body REGEXP '[Ã]';

这只在原始字节完好且只是被错误标记时才有效。如果数据真的损坏了——字节被丢弃或替换——没有备份就无法恢复。

预防

  • 始终显式声明编码,永不依赖默认值。
  • 在每个HTML文档中设置<meta charset="UTF-8">。
  • 在API响应中设置Content-Type: application/json; charset=utf-8。
  • 打开数据库连接时指定连接字符集。
  • 在编辑器中使用UTF-8,并配置版本控制系统对文本文件强制执行UTF-8。

对于超出字符集范围的编码任务——例如将二进制数据转换为文本安全表示——FileCrank的文本编码和解码工具无需任何设置即可处理Base64转换。如果需要,现有编码文章涵盖了Base64、URL编码和HTML实体。如果您正在处理配置文件并需要验证多语言字符串是否正确显示,可以直接在浏览器中检查JSON数据。

总结

字符编码错误之所以感觉神秘,是因为故障模式——字符乱码或缺失——出现在距真实原因很远的地方。但底层机制很简单:两个程序使用不同的查找表将字节转换为字符。

记住这三条规则:

  1. 始终在所有地方使用UTF-8,除非特定系统强制使用其他编码。显式设置;永不依赖默认值。
  2. 在MySQL中使用utf8mb4,而不是utf8。当用户发送表情符号时,这个区别就会体现出来。
  3. 当看到乱码时,找到边界。 寻找字节经过一个不知道其编码的系统的地方。在那个点修复声明,而不是修复下游的症状。

理解这些机制——码点、字节序列、编码声明——意味着您可以系统地分析新的编码错误,而不是猜测。下次在输出中看到é时,您将立即知道发生了什么以及去哪里查找。


所有FileCrank工具完全在您的浏览器中运行,没有文件被上传到服务器。如果您正在处理包含多语言内容的文本,请尝试JSON查看器或Base64编码/解码工具——两者都能正确处理Unicode,无需任何配置。