跳到主要内容
编码转换2026-08-297 分钟阅读

字符编码乱码排错手册:中文乱码、URL 编码、Base64 一次讲清

é”± 、锟斤拷、%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。

小结

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

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

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

排查注记

开头症状表专门设了「是不是 bug」一列,因为四类高频现象里有两个(百分号编码、Unicode 转义)根本不是错误、却被很多人当 bug 修——先判真假再谈修法,是整本手册的骨架。可逆性把真乱码再切成两半:? 和 锟斤拷 出现说明字节已丢、只能回源头重导,其余形态靠重新解读就能救回。排查流程第 3 步特意区分「重新解读」和「转换编码」:iconv 是对已经正确解读的内容做的,顺序错了会二次破坏——这也是小结强调先对表、别反复试 iconv 的原因。