2.4 编程中的编码问题
理解了编码的原理之后,我们来看看实际编程中常见的编码问题,以及如何解决这些问题。
乱码产生的根本原因
乱码的本质就是编码和解码使用的字符集不一致。
graph LR
A[字符] -->|用GBK编码| B[字节序列]
B -->|用UTF-8解码| C[乱码]
常见的乱码场景
- 文件读写乱码:写文件时用的编码和读文件时用的编码不一致
- 网络传输乱码:发送方和接收方使用的编码不一致
- 数据库乱码:数据库、表、字段的编码和应用程序使用的编码不一致
- 控制台输出乱码:程序输出的编码和控制台的显示编码不一致
- HTML页面乱码:页面实际编码和meta标签中声明的编码不一致
乱码的表现形式
不同的编码不匹配会有不同的乱码表现:
- GBK文本用UTF-8解码:中文会变成类似“䏿–‡“这样的奇怪字符,每个中文变成2-3个乱码字符
- UTF-8文本用GBK解码:中文会变成类似“涓枃“这样的乱码,每个中文变成2个乱码字符
- 缺少字体:显示为方框“□“或者问号”?“,这不是编码问题,是系统没有对应的字体
- 不可见控制字符:显示为奇怪的符号,通常是BOM或者其他控制字符导致的
常见编程语言的字符串实现
不同的编程语言对字符串和编码的处理方式不同,理解这些差异能帮你避免很多问题。
1. Python
- Python 2:字符串有两种类型:
str(字节串)和unicode(Unicode字符串),默认编码是ASCII,很容易出现编码问题 - Python 3:字符串默认是Unicode字符串(
str类型),字节序列是bytes类型,编码问题比Python 2少很多
最佳实践:
- Python 3中,处理文本一律用
str类型,处理二进制数据一律用bytes类型 - 读写文件时明确指定编码:
open('file.txt', 'r', encoding='utf-8') - 网络传输时,字符串要编码为bytes发送,接收后解码为字符串
2. Java / JavaScript / C#
- 字符串内部使用UTF-16编码
- 每个
char类型是2字节,对应UTF-16的一个代码单元 - 辅助平面的字符需要用两个
char表示(代理对)
注意点:
length()方法返回的是代码单元的数量,不是字符的数量,辅助平面字符会被算成2个长度- 处理包含表情符号等辅助平面字符时要特别注意,避免截取字符串时截断代理对
3. Go / Rust
- 字符串内部使用UTF-8编码
- 字符串本身是字节序列的包装
- 处理字符时需要显式解码为Unicode标量值
特点:
- 性能好,空间效率高
- 标准库对UTF-8处理支持完善
- 处理多字节字符时需要注意字节和字符的区别
4. C/C++
- 没有内置的字符串编码支持,
char*本质上就是字节数组 - 编码处理完全靠开发者自己或者第三方库
- C++11之后引入了
u8string、u16string、u32string等类型支持不同编码
注意点:
- 字符串处理非常容易出现编码问题
- 需要自己管理编码转换
- 不同平台的默认编码不同(Windows中文默认GBK,Linux/macOS默认UTF-8)
编码问题常见解决方案
1. 统一编码标准
最佳实践:整个技术栈统一使用UTF-8编码,从源代码、文件存储、数据库、网络传输到前端页面,全部用UTF-8,从根源上避免编码问题。
需要统一的地方:
- 源代码文件编码:都用UTF-8无BOM
- 文件存储:所有文本文件都用UTF-8编码
- 数据库:数据库、表、字段的编码都设为
utf8mb4(MySQL的utf8是伪UTF-8,只支持3字节,utf8mb4才是真正的UTF-8,支持4字节的表情符号) - 后端接口:传输JSON默认用UTF-8
- 前端页面:HTML的meta标签设置
<meta charset="UTF-8"> - 服务器配置:Nginx、Apache等服务器默认编码设为UTF-8
2. 明确指定编码
所有涉及到编码转换的地方都要明确指定编码,不要依赖系统默认编码:
- 读写文件时明确指定encoding参数
- 调用编码转换接口时明确指定源编码和目标编码
- HTTP请求的Content-Type头明确指定charset
- 数据库连接字符串指定编码
错误示例(Python):
# 不要这样写,依赖系统默认编码,不同平台行为不同
with open('file.txt', 'r') as f:
content = f.read()
正确示例:
# 明确指定编码,行为一致
with open('file.txt', 'r', encoding='utf-8') as f:
content = f.read()
3. 正确处理编码转换
编码转换的正确流程是:
字节序列 → 按源编码解码 → Unicode字符串 → 按目标编码编码 → 新的字节序列
不要直接在两种字节编码之间直接转换,一定要先解码为Unicode中间格式。
错误示例(Python 2):
# 错误:直接在GBK字节串上调用encode,会先默认用ASCII解码,导致乱码
gbk_str.decode('gbk').encode('utf-8') # 正确
gbk_str.encode('utf-8') # 错误!
4. 避免常见坑点
坑点1:Windows记事本的UTF-8带BOM
Windows的记事本保存UTF-8文件时会自动在开头加上BOM(EF BB BF),这会导致很多问题:
- Shell脚本执行报错:
#!/bin/bash前面有BOM会导致无法识别 - JSON解析错误:JSON开头有BOM会导致解析失败
- 代码编译错误:源代码开头有BOM可能导致编译失败
解决方法:不要用Windows记事本编辑代码和配置文件,用VS Code、Notepad++等专业编辑器,保存时选择UTF-8无BOM。
坑点2:MySQL的utf8不是真正的UTF-8
MySQL的utf8编码最多只支持3字节,无法存储4字节的表情符号和生僻字,要使用utf8mb4编码才是真正的UTF-8。
解决方法:
- 创建数据库时用:
CREATE DATABASE dbname DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - 数据库连接字符串指定编码:
jdbc:mysql://localhost:3306/dbname?useUnicode=true&characterEncoding=utf8mb4
坑点3:HTTP请求的编码问题
- 表单提交默认编码是
application/x-www-form-urlencoded,要用URL编码 - 文件上传用
multipart/form-data,指定编码 - 响应头要明确指定Content-Type的charset:
Content-Type: text/html; charset=utf-8
坑点4:文件名编码问题
不同操作系统的文件名编码不同:
- Windows中文默认用GBK
- Linux/macOS默认用UTF-8
跨平台传输文件时很容易出现文件名乱码,建议文件名尽量用英文和数字,避免特殊字符。
编码问题排查思路
遇到乱码问题时,按照以下步骤排查:
- 确定各个环节的编码:搞清楚数据从产生到显示经过了哪些环节,每个环节使用的编码是什么
- 查看原始二进制数据:不要只看显示的乱码,用hexdump等工具查看原始字节序列,确定实际编码
- 逐环节排查:从数据源开始,逐环节检查编码是否正确,找到编码不匹配的地方
- 修复问题:统一编码或者正确转换编码
常用工具:
hexdump/xxd:查看文件的二进制内容iconv:编码转换工具chardet:自动检测文件编码的工具(Python库)- 浏览器的开发者工具:查看HTTP请求和响应的编码
- 数据库工具:查看数据库、表、字段的编码设置
思考问题
- 你遇到过哪些印象深刻的编码问题?是怎么解决的?
- 为什么统一使用UTF-8能解决大部分编码问题?
- MySQL的utf8和utf8mb4有什么区别?为什么建议用utf8mb4?
- 如何排查一个网页显示乱码的问题?