UTF-8 的编码规则
UTF-8 把 Unicode 码位编码成 1 至 4 个字节:ASCII 区仍是单字节,拉丁扩展等用 2 字节,常用汉字用 3 字节,emoji 与增补平面字符用 4 字节。序列中首字节的高位连续的 1 表示总长度,后续字节一律以 10 开头,形成「前导字节 + 续字节」的结构。这个设计带来两个结果:ASCII 完全兼容,以及可以在字节流任意位置判断当前字节是首字节还是续字节,即自同步。UTF-8 本身没有字节序问题,因此不需要 BOM。
基本信息
| 编码长度 | 1-4 字节,对应 U+0000 至 U+10FFFF |
|---|---|
| ASCII 兼容 | U+0000-U+007F 与 ASCII 逐位相同 |
| 自同步 | 首字节表明长度,续字节以 10 开头,可快速定位字符边界 |
| 字节序 | 与字节序无关,不需要也不能依赖 BOM |
| 保留区 | U+D800-U+DFFF 代理码位不用于直接编码字符 |
| 典型用途 | 文件存储、HTTP/JSON/HTML 默认编码、源码文件 |
典型使用场景
- 新建项目统一编码:源码、配置、日志一律 UTF-8,消除跨平台差异
- 接口与数据交换:JSON 默认 UTF-8,HTTP 正文与头部也都以它为主
- 长度估算:按字节计算存储与带宽时,中文按 3 字节、emoji 按 4 字节估算
- 乱码排查:判断一段字节是否为合法 UTF-8 序列,是定位编码问题的第一步
- 跨系统迁移:从 GBK 系或 Latin-1 系迁移到 UTF-8 时做全量校验
字节层面观察
字节层面观察
python -c "print('A'.encode('utf-8'), '中'.encode('utf-8'))" # A 为 1 字节,中为 3 字节
python -c "print(len('中文'), len('中文'.encode('utf-8')))" # 字符数 2,字节数 6
python -c "print([bin(b) for b in '中'.encode('utf-8')])" # 看首字节 1110 开头、续字节 10 开头
printf '中' | xxd # 直接观察十六进制字节
iconv -f UTF-8 -t UTF-8 file.txt > /dev/null && echo valid # 非法序列会被 iconv 报出
file --mime-encoding file.txt # 辅助判断实际编码首字节与长度对照
| 首字节位模式 | 字节数 | 码位范围 | 典型字符 |
|---|---|---|---|
| 0xxxxxxx | 1 | U+0000-U+007F | 英文、数字、ASCII 标点 |
| 110xxxxx | 2 | U+0080-U+07FF | 拉丁扩展、希腊、西里尔字母 |
| 1110xxxx | 3 | U+0800-U+FFFF | 中日韩常用汉字、全角符号 |
| 11110xxx | 4 | U+10000-U+10FFFF | emoji、增补平面汉字 |
| 10xxxxxx | 续字节 | 不单独成立 | 出现在首字节之后,单独出现即非法 |
「一个字符几个字节」只是统计口径问题:UTF-8 里汉字 3 字节、emoji 4 字节,而 UTF-16 里汉字 2 字节、emoji 4 字节。字段长度限制、数据库列宽度、API 限流都要明确是按字节、按码位还是按字形簇计算。
截断 UTF-8 字节流是最常见的脏数据来源:按固定字节长度切分会切断多字节序列,尾部出现非法字节,解码时报错或被替换为问号,且原始字符无法还原。截断必须按字符边界进行。
MySQL 里的 utf8 与 utf8mb4 不是一回事:前者最多 3 字节,无法保存 emoji 与增补平面字符;需要完整 Unicode 支持时必须用 utf8mb4。类似的历史命名差异在其他系统里也存在,建库建表前要确认具体含义。