Dreamer Dreamer
首页
  • 分类
  • 标签
  • 归档
关于
GitHub (opens new window)

lycpan233

白日梦想家
首页
  • 分类
  • 标签
  • 归档
关于
GitHub (opens new window)
  • Mysql

    • 浅谈char与varchar尾随空格对比
    • utf8存储emoji处理
      • 解决方案
      • 测试数据
      • 原理
      • 有趣的发现
        • Mysql 获取 emoji 长度
        • Node.js 获取 emoji 长度
    • Mysql 查询每个班级的前几名
    • Text为什么不支持设置默认值
    • Mysql 事务级别与差异
    • Mysql SQL 优化思路
  • Node

  • Go

  • Docker

  • 后端
  • Mysql
lycpan233
2024-05-06
目录

utf8存储emoji处理

# 背景

在 to C 业务中经常会和 emoji 打交道。例如发帖场景、答题场景、搜索场景等等,总有小可爱喜欢输入一些 emoji 表情体现个性 🤣 。

emoji 通常每字符占用 4 个字节, Mysql 的 utf8 最大支持每字符 3 字节,因此会出现不兼容的问题。除此之外,还有一些生僻字也有类似问题。

注:(新版的 Mysql utf8 默认指 utf8mb4, 文中的 utf8 均指旧版的 Mysql utf8,即 utf8mb3)

常规解决方案是,在建表时采用 utf8mb4 存储,这样就不存在兼容问题。

但是存在一些历史问题,表建立时采用的是 utf8 ,改编码又会引起业务出错。以至于不得不采用一些方法对 emoji 进行兼容处理。

兼容处理方案,一般有以下几种:

  1. 通过正则匹配 emoji 将其过滤。
  2. 将文本内容 encodeURIComponent 编码,encodeURIComponent 一般不单独使用,毕竟文本整体转码以后,丧失可读性,搜索匹配也都做不到了。
  3. 通过字节长度,将不兼容的字符过滤,仅理论上可行,但实操有不兼容的风险,具体原因看 原理。

# 匹配 emoji

# 解决方案

网上有很多正则匹配 emoji 的方案,但大多都是通过指定 Unicode 字符集范围实现的,我也通过调教 AI 获得了一个示例,不过实验效果很差,最后我找到另一种比较优雅的方式实现。

结论:

/(\p{So}\p{Emoji_Modifier}?)/gu

但其有个缺点,对于部分数学符号也会被匹配到,主要是 ⌒ (圆弧)、△ (三角形)。

# 测试数据

可能你也有更好的方式匹配 emoji, 下面我提供了一些示例数据,帮助你测试。

// const str = '😄笑脸❤️红心,⌨键盘⌚手表🏳️‍🌈彩虹旗👨‍👩‍👧‍👦家庭™TM⬇️箭头☝🏽肤色符号 🍇葡萄🤘😀🫡🏳️🙌🏴󠁧󠁢󠁷󠁬󠁳󠁿🛜⛓‍💥🛞'; // emoji 混合文本, emoji 都应该被匹配。 

// const str = '~`!@#$%^&*()_+-=[]{}\\|\'\'"";:?/>.<,。,?、『』:;「」《》【】'; // 特殊符号,应该被保留

const str = '∪∩∈∉⊆⊇⊂⊃∅∧∨¬⇒⇔=≠<>≤≥≈≡+-×÷√∛∑∏∫∂∆∞∠⊥‖⌒⊙△αβγδεζηθικλμνξοπρστυφχψω≮≯∷/∮∝∵∴()【】{}ⅠⅡ⊕∥Δ'; // 数学符号,应该被保留

console.log(str); // 源数据
console.log(str.replace(/(\p{So}\p{Emoji_Modifier}?)/gu, ' ')); // 匹配后替换为空格便于对比

1
2
3
4
5
6
7
8
9

# 原理

Regexp: /(\p{So}\p{Emoji_Modifier}?)/gu

其中 g 代表全局, u 代表 Unicode。

而 \p{XXX} 这个语法是通过 Unicode 字符类别进行匹配的,例如 \p{N} 匹配 Number 类别。

\p{So}, 匹配的类别是 Symbol(符号类别)里的 o(other)类。So 里面包含了大部分 emoji 。

对于一些特殊的 emoji 如 ☝🏽 其是 ☝ + 🏽 (手指 + 肤色)构成的,\p{So} 只能匹配单 Unicode 字符的 emoji,即只能匹配到 ☝ ,所以引入了后半段。

\p{Emoji_Modifier}, 匹配的类别是 Emoji_Modifier(表情修饰符类别)。Emoji_Modifier 里包含了 emoji 的修饰符,如 🏻 🏼 🏽 🏾 🏿。

# 有趣的发现

通过上面的示例,我们大概知道 emoji 占用超过 3 个字节,导致 utf8 无法正常存储。还有一部分 emoji 是通过组合得来的,如 ☝🏽。

这时候我们进行一个提问 😄、☝🏽、👨‍👩‍👧‍👦、🏳️‍🌈 分别长度是多少?

你可以想一想,或者用你常用的语言试试。看看结果和我的是否一致。


有结论了吗? 首先请允许我向你道歉,我没有明确『长度』的概念,究竟是字符长度还是字节长度。

(如果你对于这个概念不太熟悉,可以看我另一篇文章 VARCHAR的长度怎么计算? VARCHAR(255)与VARCAHR(256)的区别在哪里? (opens new window))

好了,现在我们通过两种方式,去分析问题的答案。

  1. Mysql
  2. Node.js

# Mysql 获取 emoji 长度

SELECT length('你'), char_length('你');
-- 3	1

SELECT length('😄'), char_length('😄'); 
-- 4	1
-- inset into 😄  ->  ?

SELECT length('☝🏽'), char_length('☝🏽');
-- 7	2
-- inset into ☝🏽  ->  ☝?

SELECT length('👨‍👩‍👧‍👦'), char_length('👨‍👩‍👧‍👦');
-- 25	7
-- inset into 👨‍👩‍👧‍👦  ->  ????

SELECT length('🏳️‍🌈'), char_length('🏳️‍🌈');
-- 14	4
-- inset into 🏳️‍🌈  ->  ??

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这里借用了 Mysql 的两个内置函数 length() 和 char_length(),分别获取字节长度和字符长度。

😄 占用了 1 个字节,且 1 个字节需要用 4 个字符标识,所以 Mysql utf8 不支持,当尝试插入时,会变成 ? 。

☝🏽、👨‍👩‍👧‍👦、🏳️‍🌈 都占用了多个字节,其中比较特别的是 ☝🏽 它居然兼容了一半...

# Node.js 获取 emoji 长度

JavaScript 中因历史原因的问题,length 本身存在一些问题。

例如: 😄.length // 2

大家可以直接打开浏览器的控制台体验一下,这里不多赘述。

所以这里我们通过 Node 的 Buffer 对象将字符串转为 utf8 编码后再看效果。

测试代码如下,大家可以自己体验效果:

const str = '☝🏽';
console.log(str);
console.log('length: ', str.length);
console.log('buffer: ', Buffer.from(str, 'utf-8').length);

console.log('for i :');
for (let i = 0; i < str.length; i++) {
  console.log(`i: ${i}, char: ${str[i]}, length: ${str[i].length}, buffer: ${Buffer.from(str[i], 'utf-8').length}`);
}

console.log('for of :');
let i = 0;
for (const el of str) {
  console.log(`i: ${i++}, char: ${el}, length: ${el.length}, buffer: ${Buffer.from(el, 'utf-8').length}`);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在示例中,我用了 for i 和 for of 两种遍历方式,因为 length 的问题,导致 for i 的结果看起来很鬼畜,但好在 for of 是正常的。

😄
for of :
i: 0, char: 😄, length: 2, buffer: 4
1
2
3
☝🏽
for of :
i: 0, char: ☝, length: 1, buffer: 3
i: 1, char: 🏽, length: 2, buffer: 4
1
2
3
4
👨‍👩‍👧‍👦
for of :
i: 0, char: 👨, length: 2, buffer: 4
i: 1, char: ‍, length: 1, buffer: 3
i: 2, char: 👩, length: 2, buffer: 4
i: 3, char: ‍, length: 1, buffer: 3
i: 4, char: 👧, length: 2, buffer: 4
i: 5, char: ‍, length: 1, buffer: 3
i: 6, char: 👦, length: 2, buffer: 4
1
2
3
4
5
6
7
8
9
🏳️‍🌈
for of :
i: 0, char: 🏳, length: 2, buffer: 4
i: 1, char: ️, length: 1, buffer: 3
i: 2, char: ‍, length: 1, buffer: 3
i: 3, char: 🌈, length: 2, buffer: 4
1
2
3
4
5
6

从输出结果中我们可以得出结论, for of 的循环次数等同于 emoji 的字符长度,而 Buffer.form().length 也可以很好的反应出,每个字符的字节长度。

同时,有部分占用 3 字节的字符,utf8 也是不支持的,而且我们看到的一个 emoji,可能对应多个字符。因此通过字节长度过滤不兼容的 emoji 不太靠谱。

编辑 (opens new window)
上次更新: 2025/04/15, 03:48:14
浅谈char与varchar尾随空格对比
Mysql 查询每个班级的前几名

← 浅谈char与varchar尾随空格对比 Mysql 查询每个班级的前几名→

最近更新
01
docker基础概念
02-26
02
js 获取变量准确类型
02-19
03
Mysql SQL 优化思路
02-18
更多文章>
Theme by Vdoing | Copyright © 2023-2025 Dreamer | MIT License
粤ICP备2025379918号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式