é”± 、锟斤拷、%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\u6587 是 Unicode 转义表示,在 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。
小结
判断编码问题的核心是分清"表示"和"损坏":
%XX、\uXXXX→ 是编码表示,解码即可,不是 bug?、锟斤拷→ 数据已损坏,回源头重来- 其他乱码 → 字节完好,用正确编码重新解读能救回
- 预防 → 全链路 UTF-8 + 永远显式指定编码
下次再遇到乱码,先按开头那张表对号入座,比反复试 iconv 快得多。
相关工具
相关文章
中文 URL 编码后变成 %E4%B8%AD 这种 %XX,正常吗?
网址里的中文变成一串 %E4%B8%AD 这样的百分号编码正常吗?为什么需要编码、编码规则是什么、怎么解码回来、前端如何处理?附本站 URL 编解码工具。
Base64 解码后中文变成乱码 / 问号(�)怎么办?
Base64 解码后中文变成乱码或黑色问号方块怎么办?讲清"编码的是字节不是字符"这一根因,并给出命令行、代码和在线工具三种正确解法,全程本地处理。
如何批量把多个文件或文本转成 Base64(不走脚本也能做)
需要把几十张图片或一堆字符串一次性转成 Base64?本文讲解批量 Base64 编码的方法,并一步步用本地批量工具处理,避免写脚本、避免文件上传泄露。