不喜欢广告? 无广告 今天

UTF-8 和 Unicode 为什么那个表情符号破坏了你的数据库

更新于

您的应用程序插入了一个表情符号,MySQL 报错“字符串值不正确”。原因如下——代码点与字节、MySQL 的 utf8 与 utf8mb4 的误区、JavaScript 的代理对,以及如何真正解决该问题。

UTF-8 和 Unicode:为什么那个表情符号破坏了你的数据库 2
广告 移除?

你的应用程序运行正常。然后一个用户在文本框中输入了一个表情符号,MySQL 报错 Incorrect string value: '\xF0\x9F\x98\x80' for column 'bio'。或者表情符号无声消失。或者整个 INSERT 操作失败,导致数据丢失。这一切都源于数据库列无法处理的一个四字节字符。

这不是 MySQL 的缺陷,也不是 PHP 的 bug。这是 Unicode 转换为 UTF-8 编码工作方式的结果,一旦你理解了这一点,你就不会再被它惊到了。

代码点与字节:实际差异

Unicode 为每个字符分配一个 代码点 —— 一个数字。字母 A 是 U+0041,欧元符号是 U+20AC,😀 表情符号是 U+1F600。这是字符的抽象身份。

UTF-8 是一种 编码 —— 一种将代码点存储为字节的方式。其巧妙之处在于 UTF-8 是可变宽度的:根据代码点的值,它使用 1 到 4 个字节。正是这种设计使其与 ASCII 向后兼容(所有 ASCII 字符在 UTF-8 中都是 1 字节),同时又能编码所有存在的字符。

编码规则:

  • U+0000 到 U+007F(ASCII)→ 1 字节
  • U+0080 到 U+07FF(拉丁扩展、阿拉伯语、希伯来语等)→ 2 字节
  • U+0800 到 U+FFFF(大部分中日韩字符、标点符号、符号)→ 3 字节
  • U+10000 到 U+10FFFF(表情符号、罕见文字、数学符号)→ 4 字节

这就是为什么 😀 表情符号(U+1F600)需要 4 个字节:其代码点高于 U+FFFF。

UTF-8 字节大小:参考表

以下是常见字符实际占用的字节数:

字符描述Unicode 代码点UTF-8 字节(十六进制)字节数
A拉丁大写字母 AU+0041411
é拉丁字母 e 带锐音符U+00E9C3 A92
欧元符号U+20ACE2 82 AC3
中文字符“中”U+4E2DE4 B8 AD3
😀微笑表情符号U+1F600F0 9F 98 804
🔥火焰表情符号U+1F525F0 9F 94 A54
𝕳数学弗拉克尔 HU+1D573F0 9D 95 B34

要亲自验证这一点,请使用 字符串长度计算器 —— 它会显示你粘贴的文本中的字符数量和字节数。粘贴 😀 你会发现 1 个字符但占 4 个字节。

MySQL 的 utf8 误区

这里就是开发者常遇到的问题。MySQL 有一个名为 utf8的字符集。听起来合理。但实际上是错误的——MySQL 的 utf8 仅支持最多 3 字节序列。表情符号(4 字节)不被支持。

MySQL 中真正的完整 UTF-8 字符集是 utf8mb4 (从 MySQL 5.5.3 开始引入,2010 年发布)。如果你的列使用了 utf8 而有人插入了一个表情符号,MySQL 要么会静默截断数据,要么会抛出错误:

Incorrect string value: '\xF0\x9F\x98\x80' for column 'bio' at row 1

解决方法:

-- Convert the table
ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- Or for a specific column
ALTER TABLE users MODIFY bio TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- And set your connection charset
SET NAMES utf8mb4;

同时,请更新你的应用程序数据库连接配置。在 MySQL PDO 中:

$pdo = new PDO($dsn, $user, $pass, [
    PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4"
]);

VARCHAR(255) 的陷阱

VARCHAR(255) 在 MySQL 中,意味着 255 个字符,而不是 255 个字节——但单行的存储限制是按字节计算的。对于 utf8mb4,每个字符最多占用 4 个字节,因此一个 VARCHAR(255) 列最多预留 1,020 个字节。这一点在使用 InnoDB 默认前缀索引限制(767 字节)对 varchar 列进行索引时尤为重要:

-- This fails on older MySQL with default innodb_large_prefix=OFF
CREATE INDEX idx_email ON users (email);  -- email is VARCHAR(255) utf8mb4

-- Fix: use a prefix index
CREATE INDEX idx_email ON users (email(191));  -- 191 * 4 = 764 bytes, under 767

-- Or enable large prefixes (MySQL 5.7+, on by default in 8.0)
-- Set innodb_large_prefix = ON in my.cnf

JavaScript 和代理对问题

JavaScript 内部使用的是 UTF-16,而不是 UTF-8。而 UTF-16 对于高于 U+FFFF 的代码点也有自己的多单位编码: 代理对 —— 两个 16 位代码单元共同表示一个字符。

这意味着 String.length 在 JavaScript 中计数的是 UTF-16 代码单元,而不是字符:

'😀'.length        // → 2 (two UTF-16 surrogate code units)
[...'😀'].length   // → 1 (spread operator uses Unicode code points)

// Checking the character at index 0
'😀'[0]            // → '\uD83D' (the high surrogate, not the emoji)
'😀'.codePointAt(0) // → 128512 (0x1F600, correct)

对于需要字符感知的字符串操作,应使用扩展运算符或 Intl.Segmenter:

// Count actual grapheme clusters
const segmenter = new Intl.Segmenter();
const chars = [...segmenter.segment('👨‍👩‍👧‍👦')];
chars.length     // → 1 (family emoji is one grapheme cluster)
'👨‍👩‍👧‍👦'.length  // → 11 (UTF-16 code units)

家庭表情符号示例值得暂停思考。 👨‍👩‍👧‍👦 由四个表情符号通过零宽度连接符(U+200D)连接而成。一个简单的 .length 会给出 11,实际的图形簇数量是 1。如果在实现字符限制时,基于 String.length 的限制将表现出异常行为,当用户输入表情符号序列时。

如何在实践中检查编码

Python

s = '😀'
print(len(s))                    # 1 (Python 3 counts code points)
print(len(s.encode('utf-8')))    # 4 bytes
print(s.encode('utf-8').hex())   # f09f9880

PHP

$s = '😀';
echo strlen($s);          // 4 (bytes, not characters)
echo mb_strlen($s);       // 1 (characters)
echo mb_strlen($s, '8bit'); // 4 (bytes, explicit)

strlen() 在 PHP 中计数的是字节,而不是字符。这会让 PHP 开发者在处理多字节字符串时不断遇到问题——一个包含 10 个表情符号的字符串会报告长度为 40。当你关心字符数量时,请使用 mb_strlen()

MySQL

-- Check charset of a table
SHOW CREATE TABLE users\G

-- Check charset of a specific column
SELECT CHARACTER_SET_NAME, COLLATION_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'bio';

-- Character count vs byte count
SELECT CHAR_LENGTH('😀'), LENGTH('😀');
-- → 1, 4

快速检查

如果你想查看任意文本的字节与字符数量,而无需编写代码,可以使用 字符串长度计算器 它会立即处理——粘贴任意文本,它会并排显示字符数量、词数和字节数。

编码错误检查清单

  • MySQL 字符集:utf8mb4XML没有数字类型——所有内容都是文本。你的价格字段将是 utf8吗?使用 SHOW CREATE TABLE.
  • 检查 连接配置: SET NAMES utf8mb4你的应用程序是否发送
  • ?检查你的 DSN 或连接配置。 PHP strlen 与 mb_strlen:
  • 你是否在需要字符计数的地方使用了字节计数函数? JavaScript .length:
  • 你是否在需要图形簇的地方计数了代码单元? HTTP 头部: Content-Type: text/html; charset=utf-8?
  • 你的响应是否发送了 文件编码:
想要享受无广告的体验吗? 立即无广告

安装我们的扩展

将 IO 工具添加到您最喜欢的浏览器,以便即时访问和更快地搜索

添加 Chrome 扩展程序 添加 边缘延伸 添加 Firefox 扩展 添加 Opera 扩展

记分板已到达!

记分板 是一种有趣的跟踪您游戏的方式,所有数据都存储在您的浏览器中。更多功能即将推出!

广告 移除?
广告 移除?
广告 移除?

新闻角 包含技术亮点

参与其中

帮助我们继续提供有价值的免费工具

给我买杯咖啡
广告 移除?