Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

2.4 编程中的编码问题

理解了编码的原理之后,我们来看看实际编程中常见的编码问题,以及如何解决这些问题。

乱码产生的根本原因

乱码的本质就是编码和解码使用的字符集不一致

graph LR
A[字符] -->|用GBK编码| B[字节序列]
B -->|用UTF-8解码| C[乱码]

常见的乱码场景

  1. 文件读写乱码:写文件时用的编码和读文件时用的编码不一致
  2. 网络传输乱码:发送方和接收方使用的编码不一致
  3. 数据库乱码:数据库、表、字段的编码和应用程序使用的编码不一致
  4. 控制台输出乱码:程序输出的编码和控制台的显示编码不一致
  5. 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之后引入了u8stringu16stringu32string等类型支持不同编码

注意点

  • 字符串处理非常容易出现编码问题
  • 需要自己管理编码转换
  • 不同平台的默认编码不同(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

跨平台传输文件时很容易出现文件名乱码,建议文件名尽量用英文和数字,避免特殊字符。

编码问题排查思路

遇到乱码问题时,按照以下步骤排查:

  1. 确定各个环节的编码:搞清楚数据从产生到显示经过了哪些环节,每个环节使用的编码是什么
  2. 查看原始二进制数据:不要只看显示的乱码,用hexdump等工具查看原始字节序列,确定实际编码
  3. 逐环节排查:从数据源开始,逐环节检查编码是否正确,找到编码不匹配的地方
  4. 修复问题:统一编码或者正确转换编码

常用工具

  • hexdump/xxd:查看文件的二进制内容
  • iconv:编码转换工具
  • chardet:自动检测文件编码的工具(Python库)
  • 浏览器的开发者工具:查看HTTP请求和响应的编码
  • 数据库工具:查看数据库、表、字段的编码设置

思考问题

  1. 你遇到过哪些印象深刻的编码问题?是怎么解决的?
  2. 为什么统一使用UTF-8能解决大部分编码问题?
  3. MySQL的utf8和utf8mb4有什么区别?为什么建议用utf8mb4?
  4. 如何排查一个网页显示乱码的问题?