Skip to content
编码转换2026-08-297 分钟阅读

é”±锟斤拷%E4%B8%AD%E6%96%87\u4e2d\u6587

这四个东西开发者几乎都见过,但它们成因完全不同,修法也完全不同。最麻烦的是:其中有两个(百分号编码和 Unicode 转义)根本不是错误,只是正常的数据表示——很多人却把它们当 bug 修。

本手册按症状分类,先帮你判断"这是不是真出问题了",再给对应修法。

先按症状定位

| 你看到的现象 | 是不是 bug | 原因 | 跳到 | |---|---|---|---| | 中文变成 ???? | ✅ 是 | 写入时编码不支持中文 | 类型 1 | | 中文变成 é”± / å¼ ä¸ | ✅ 是 | UTF-8 字节被按 GBK/Latin-1 解读 | 类型 1 | | 锟斤拷 / æ–‡å— | ✅ 是(多不可逆) | 编码被来回转换过 | 类型 1 | | 中文变 %E4%B8%AD%E6%96%87 | ❌ 不是 | URL 百分号编码,合法 | 类型 2 | | 中文变 \u4e2d\u6587 | ❌ 不是 | Unicode 转义,合法 | 类型 3 | | Base64 解码后中文乱码 | ✅ 是 | 编码前后字符集不一致 | 类型 4 | | 空格变 +%20 | ❌ 不是 | URL 编码的空间约定 | 类型 2 |

一句话判断:内容能靠"换编码重新解读"恢复原来的中文 → 那是编码问题;内容本身是一种编码后的表示(百分号、\u)→ 那是正常的,解码即可。


编码基础速查(看懂乱码的前提)

三个概念常被混为一谈,其实分工明确:

| 概念 | 是什么 | 类比 | |---|---|---| | Unicode | 字符集,给每个字符一个编号(如"中"= U+4E2D) | 字典里的字和编号 | | UTF-8 | 编码规则,把编号变成字节("中"→ 3 字节 E4 B8 AD) | 把编号写成摩斯电码 | | GBK | 另一种编码规则,中文用 2 字节 | 另一套摩斯电码 |

乱码的本质:用 A 规则编码的字节,被用 B 规则解读。字节没变,解读错了。

"中" --UTF-8编码--> E4 B8 AD --按GBK解读--> "涓" (乱码)

⚠️ 常见误解:以为"乱码是数据坏了"。通常字节完好无损,只是解读方式错了——所以大多数乱码是可恢复的(用正确编码重新解读即可)。

想系统理解 ASCII / Unicode / UTF-8 的关系,见 ASCII vs Unicode vs UTF-8 详解


类型 1:真乱码(编码不一致)

症状对照表

| 乱码形态 | 成因 | 可逆吗 | |---|---|---| | ???? | 写入时用 ASCII/Latin-1 等不支持中文的编码,中文被替换成问号 | ❌ 不可逆,原始字节已丢失 | | é”± | UTF-8 字节被按 Latin-1(ISO-8859-1)解读 | ✅ 可逆 | | å¼ ä¸ | UTF-8 字节被按 Latin-1 解读后再存成 UTF-8 | ✅ 可逆 | | 涓枃 | UTF-8 字节被按 GBK 解读 | ✅ 可逆 | | 锟斤拷 | UTF-8 → GBK → 再转回,中间出现 EF BF BD 替换字符 | ❌ 不可逆 |

关键结论

  • ?锟斤拷 出现,说明数据已经丢了,必须回到源头重新导出。
  • 其他形态(字节完好)通常能靠"用正确编码重新解读"救回来。

修复方法

# 1. 先确认文件真实编码(别猜)
file -I data.csv
# 或
enca -L zh_CN data.csv

# 2. 转换编码(GBK -> UTF-8)
iconv -f GBK -t UTF-8 data.csv > data.utf8.csv

# 3. 转换失败说明有无法映射的字符,可用 //IGNORE 跳过(会丢数据)
iconv -f GBK -t UTF-8//IGNORE data.csv > data.utf8.csv
# Python:按正确编码读取
with open('data.csv', encoding='gbk') as f:      # 关键:显式指定
    content = f.read()

# 已被错误解读的情况(mojibake):反向操作救回
broken = 'æ–‡å—'
fixed = broken.encode('latin-1').decode('utf-8')  # -> 正常中文

根因预防

黄金法则:全链路统一 UTF-8,并且永远显式指定编码,不要依赖系统默认值。

| 环节 | 要做的 | |---|---| | 文件存储 | 统一 UTF-8(无 BOM 更佳) | | 数据库 | 库/表/连接三处都设 utf8mb4 | | HTTP | 响应头 Content-Type: text/html; charset=utf-8 | | HTML | <meta charset="utf-8"> | | 编辑器 | 保存为 UTF-8,关闭"自动检测编码" |

MySQL 特别注意:utf8 不是真正的 UTF-8(最多 3 字节,存不了 emoji),必须用 utf8mb4


类型 2:URL 百分号编码(不是乱码)

浏览器地址栏里中文变成 %E4%B8%AD%E6%96%87,这是 URL Encoding(百分号编码),是标准行为,不是错误

  • 的 UTF-8 字节是 E4 B8 AD → 编码后 %E4%B8%AD
  • 一个中文字 = 3 个字节 = 3 个 %XX

什么时候需要手动处理

| 场景 | 处理 | |---|---| | 拼接 URL 参数 | 必须编码,否则中文/特殊字符会破坏 URL 结构 | | 看到 %XX 想看原文 | 解码即可 | | 空格变 + 还是 %20 | 查询串里两者都可,+ 是历史约定 |

// JavaScript
encodeURIComponent('中文')      // '%E4%B8%AD%E6%96%87'
decodeURIComponent('%E4%B8%AD') // '中'

// 注意 encodeURI 和 encodeURIComponent 的区别:
encodeURI('https://a.com/中文')           // 保留 : / ? 等,用于整条 URL
encodeURIComponent('https://a.com/中文')  // 全部编码,用于参数值
from urllib.parse import quote, unquote
quote('中文')                 # '%E4%B8%AD%E6%96%87'
unquote('%E4%B8%AD%E6%96%87') # '中文'

URL 编码解码工具 可以直接看到每个字符的编码结果。

👉 完整说明见 中文 URL 编码:百分号是什么


类型 3:Unicode 转义(不是乱码)

\u4e2d\u6587Unicode 转义表示,在 JSON、Java properties、JS 字符串里都合法,解析后就是"中文"

| 出现位置 | 含义 | |---|---| | JSON 里 | 合法,JSON 标准允许 | | Java .properties | 默认就是这种(ISO-8859-1 历史遗留) | | Python 序列化输出 | 因为 ensure_ascii=True 默认值 |

想让它直接显示中文

json.dumps(data, ensure_ascii=False)   # Python 关闭转义
JSON.stringify({name: '中文'})  // JS 默认输出 "中文",不转义
# Java properties 转 UTF-8
native2ascii -reverse -encoding UTF-8 app.properties app.utf8.properties

👉 JSON 场景详见 JSON 中文变 \uXXXX 转义怎么办


类型 4:Base64 与中文

Base64 本身只处理字节,不关心字符集——所以中文问题都出在编码前后的字符集不一致

中文 --(UTF-8)--> 字节 --(Base64)--> 5Lit5paH

乱码成因:编码端用 UTF-8,解码端按 GBK 把字节转回字符。

正确做法

// 编码:先明确转成 UTF-8 字节,再 Base64
const bytes = new TextEncoder().encode('中文');         // UTF-8 字节
const b64 = btoa(String.fromCharCode(...bytes));

// 解码:Base64 -> 字节 -> 按 UTF-8 解读
const bin = atob(b64);
const bytes2 = Uint8Array.from(bin, c => c.charCodeAt(0));
new TextDecoder('utf-8').decode(bytes2);               // '中文'
import base64
base64.b64encode('中文'.encode('utf-8'))               # 显式 UTF-8
base64.b64decode(token).decode('utf-8')                # 显式 UTF-8

⚠️ btoa('中文') 会直接抛错(Character Out Of Range),因为它只能处理 Latin-1。必须先转 UTF-8 字节。

Base64 编解码工具 可快速验证。

👉 详见 Base64 解码后中文乱码怎么办Base64 中文编码完整指南


通用排查流程

遇到编码问题,按顺序做这四步:

1. 先判断"是不是真坏了"%XX\uXXXX?→ 不是 bug,解码即可。是 ?锟斤拷?→ 数据已丢,回源头重导。

2. 确认字节,而不是看显示 显示会骗人(终端、编辑器各有自己的编码设置)。看原始字节:

hexdump -C file.txt | head        # 看最原始字节
xxd file.txt | head

"中"的 UTF-8 字节是 e4 b8 ad,GBK 是 d6 d0——看字节就知道真实编码。

3. 用正确编码重新解读(不要转换) 如果字节完好,重新解读能救回;而转换编码(iconv)是对已经正确解读的内容做的。顺序错了会二次破坏。

4. 全链路统一 UTF-8 并显式指定 不要依赖系统默认编码。每个读写环节都写明 encoding='utf-8'


排错工具箱

| 用途 | 工具 | |---|---| | URL 百分号编码 / 解码 | URL 编码解码 | | Base64 编码 / 解码 | Base64 编解码 | | Unicode 码点与字符互转 | Unicode 编码解码 | | HTML 实体编码 / 解码 | HTML 实体编码 | | Base32 / Base58 编码 | Base32/Base58 编码 | | 对比两段文本差异 | 文本对比 |

以上工具全部在浏览器本地运行,你可以放心粘贴含敏感信息的内容——数据不会离开你的机器,按 F12 打开 Network 面板可自行验证。


常见问题

Q:为什么同一个文件,我这边正常、同事那边乱码? 多半是编辑器/系统的默认编码不同。Windows 记事本历史上默认 GBK(现已改),Linux/macOS 默认 UTF-8。解决方法永远是显式指定编码,别依赖默认值。

Q:锟斤拷 能救回来吗? 基本不能。它是 UTF-8 → GBK 转换时产生的替换字符 EF BF BD,原始字节在转换中已被丢弃。只能回到数据源重新导出。

Q:Base64 里为什么没有中文概念? 因为 Base64 只处理字节。它不知道你喂进来的是文本还是图片。"中文"这件事在进入 Base64 之前(字符→字节)和之后(字节→字符)才存在。

Q:URL 里的空格应该是 + 还是 %20 查询字符串(? 后面)里两者通常都能被服务端识别,+ 是历史约定;路径部分必须用 %20。稳妥做法是统一用 encodeURIComponent(它产出 %20)。

Q:MySQL 存 emoji 报错 Incorrect string value 因为用了 utf8 而非 utf8mb4。MySQL 的 utf8 最多 3 字节,存不了 4 字节的 emoji。把库、表、连接字符集全部改成 utf8mb4

小结

判断编码问题的核心是分清"表示"和"损坏"

  1. %XX\uXXXX → 是编码表示,解码即可,不是 bug
  2. ?锟斤拷 → 数据已损坏,回源头重来
  3. 其他乱码 → 字节完好,用正确编码重新解读能救回
  4. 预防 → 全链路 UTF-8 + 永远显式指定编码

下次再遇到乱码,先按开头那张表对号入座,比反复试 iconv 快得多。


广告