爬虫抓回来的数据全是 ???,MySQL 存中文显示乱码,PHP strlen("你好") 返回 6 而不是 2。这类问题 99% 的根源不在数据库,不在浏览器,而在于没搞清 ASCII、Unicode、UTF-8 三者到底是什么关系。
这篇把三者拆开讲透,让你下次遇到字符问题能立刻判断是字符集问题还是编码问题。想直接查看或转换字符,用我们的 Unicode 编解码工具,可以显示任意文本的码位和 UTF-8 字节。
ASCII:128 个字符的 7-bit 时代
ASCII(American Standard Code for Information Interchange)诞生于 1960 年代,用 7 个 bit 表示一个字符,共 128 个码位:
| 范围 | 内容 | 数量 | |------|------|------| | 0-31 | 控制字符(换行、制表等) | 32 | | 32-126 | 可打印字符(字母、数字、符号) | 95 | | 127 | DEL | 1 |
'A' = 65 = 0x41
'a' = 97 = 0x61
'0' = 48 = 0x30
' ' = 32 = 0x20
ASCII 解决了英文的存储问题,但仅限英文。德文的 ä、中文的"中"、日文的"あ"、Emoji,全部无能为力。
Unicode:覆盖全世界的字符集
Unicode 是一张大表,给全世界几乎所有字符都分配了一个唯一编号(码位 code point)。范围从 U+0000 到 U+10FFFF,理论上能容纳 110 多万个字符,目前已分配超过 14 万。
U+0041 A (Latin Capital Letter A)
U+4E2D 中 (CJK Unified Ideograph)
U+1F600 😀 (Grinning Face)
U+1F4A9 💩 (Pile of Poo)
注意 Unicode 只规定"哪个字符对应哪个编号",没有规定这个编号在内存里怎么存。把它存成字节序列的方式叫"编码",UTF-8 就是其中一种,也是最流行的一种。
UTF-8:Unicode 的可变长度编码
UTF-8 是 Unicode 的一种实现方式,用 1 到 4 个字节存一个码位:
| 字节长度 | 码位范围 | 典型字符 | 例子 |
|----------|----------|----------|------|
| 1 字节 | U+0000 - U+007F | ASCII | A → 0x41 |
| 2 字节 | U+0080 - U+07FF | 拉丁扩展、希腊、西里尔 | é → 0xC3 0xA9 |
| 3 字节 | U+0800 - U+FFFF | CJK 中文日韩 | 中 → 0xE4 0xB8 0xAD |
| 4 字节 | U+10000+ | Emoji、古文字 | 😀 → 0xF0 0x9F 0x98 0x80 |
变长编码的好处:英文文本和 ASCII 一样紧凑,CJK 文本虽然多一个字节但能正确存储,没有浪费。
UTF-8 向后兼容 ASCII
这是 UTF-8 设计上最聪明的地方。前 128 个码位(U+0000 到 U+007F)在 UTF-8 里用 1 个字节表示,而且字节值和 ASCII 完全一样:
ASCII 'A' = 0x41
UTF-8 'A' = 0x41 # 同一个字节
任何纯 ASCII 文本,本身就是合法的 UTF-8 文本。这就是为什么 HTML、Email、HTTP 协议至今默认 UTF-8,旧的英文系统不用改一行代码就能继续跑。
字符 vs 字节:以 "你好" 为例
"你好" 是 2 个字符,但 UTF-8 编码后是 6 个字节:
字符: 你 好
码位: U+4F60 U+597D
UTF-8: E4 BD A0 E5 A5 BD
字节: ----- 3 ----- ----- 3 -----
$ echo -n "你好" | xxd
00000000: e4bd a0e5 a5bd ......
$ echo -n "你好" | wc -c
6
PHP 和 C 里的 strlen 数的是字节,所以 strlen("你好") 返回 6。要用 mb_strlen 才能得到字符数 2:
<?php
$s = "你好";
echo strlen($s); // 6 (字节数)
echo mb_strlen($s); // 2 (字符数)
这个区别是 90% 字符相关 bug 的来源。前端 String.length 在 JavaScript 里数的是 UTF-16 码元,对 😀 这种 4 字节字符会返回 2,又是另一层坑。
hex dump 对比
"A" hex: 41 # 1 字节
"中" hex: E4 B8 AD # 3 字节
"😀" hex: F0 9F 98 80 # 4 字节
"A中" hex: 41 E4 B8 AD # 混合编码,共 4 字节
UTF-8 的解码器靠首字节的高位判断后续字节数:
- 首字节
0xxxxxxx:1 字节字符 - 首字节
110xxxxx:2 字节字符,后跟 1 个10xxxxxx - 首字节
1110xxxx:3 字节字符,后跟 2 个10xxxxxx - 首字节
11110xxx:4 字节字符,后跟 3 个10xxxxxx
这就是为什么 UTF-8 能自我同步:从字节流中间任意位置开始,最多回溯几个字节就能定位字符边界。
常见编码 bug
1. 多字节字符被截断
// 错误:按字节长度截中文字符串可能切断一个 3 字节序列
const s = "你好世界";
s.substr(0, 3); // 可能拿到半个"你"
现代 JavaScript slice 按字符数操作,安全。但 Buffer 操作、fetch 流处理、字节级截断还是容易踩坑。
2. MySQL utf8 vs utf8mb4
MySQL 的 utf8 字符集最多存 3 字节字符,存 Emoji 会失败。要用 utf8mb4。MySQL 8.0 已经把默认改成了 utf8mb4,但老库还很多。
3. BOM 导致首字符识别错
BOM(U+FEFF)放在文件开头,告诉读取器编码是 UTF-8。但 BOM 会作为内容的一部分出现在 PHP header() 之前的输出里,导致 "headers already sent"。多数情况下不要 BOM,存为 UTF-8 without BOM。
何时该考虑编码
- 存数据库前:确认表字符集是 utf8mb4,连接编码也是 utf8mb4
- 写 HTTP 响应:
Content-Type: application/json; charset=utf-8 - 解析二进制协议:明确每个字段的字节序和编码
- 跨语言传字符串:永远 UTF-8,不要用本地编码
在本地完成编码调试
调编码问题需要 hex dump、码位查询、字节计数。这些操作最好别把数据贴到陌生网站,尤其是带敏感内容的字符串。
Unicode 编解码工具 能列出每个字符的码位和字节序列,Base64 编解码工具 帮你确认 Base64 里嵌的中文是否完整还原,字符频率统计 能在分析大段文本时验证字符是否正确。全部在浏览器本地运行,数据不上传。
下次遇到乱码,先看 hex,再看码位,最后才是字符集。问题往往比想象中简单。
relatedTools
相关文章
最佳在线 JWT 解码工具推荐(2026)
对比主流在线 JWT 解码工具,从签名校验、隐私保护、功能完整度等维度评测。DevToolkit Pro 凭借本地解码和 HS256 签名校验脱颖而出。
最佳在线 Base64 编解码工具推荐(2026)
对比主流在线 Base64 编解码工具,从 Unicode 支持、隐私保护、使用便捷性等维度评测。DevToolkit Pro 凭借本地运行和 Unicode 安全编码成为首选。
在线 Base64 转图片工具:将编码字符串还原为图片文件
学习如何将 Base64 编码字符串还原为图片文件。了解 Base64 解码原理、常见的 Base64 图片格式,以及在 Web 开发中的实际应用场景。