emoji表情包字符分割问题引出的编码知识

2024-06-06 05:18

本文主要是介绍emoji表情包字符分割问题引出的编码知识,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

经验分享:一个小小emoji尽然牵扯出来这么多东西?

目录

  • 问题
  • 概念常识
    • utf8mb4
    • ASCII码
    • Unicode
      • 平面
      • 表示范围
      • 实现方式
  • 解决emoji截取的问题
    • UTF-16
      • surrogate
    • emoji截取异常原因
    • 问题解决
  • 总结

问题

分享工作中一个真实的案例:
要求显示用户昵称时只能显示第一位和最后一位,其他的用※代替。
例如输入:🐳🐳🐠,输出:🐳***🐠
发现用户昵称包含emoji表情时就会出问题,切割的数据会有问号显示!!

模拟的示例代码如下:
在这里插入图片描述
输出:
在这里插入图片描述

概念常识

要解决这些问题,就必须要铺垫一些基础知识

utf8mb4

一般我们在数据库创建表时都会默认使用这种编码格式:
在这里插入图片描述
当我们想存储emoji数据到数据库中,那么数据库的格式就需要指定为utf8mb4了,要不然存储就会报错了。所以在很多公司的db规范中,数据库默认编码必须为utf8mb4
为何utf8不行而utf8mb4就行?这里面到底有什么弯弯道道?

这里面涉及到unicode相关知识,我们下面会提到,大家继续看。

在mysql 5.5 之前,utf8编码只支持1-3个字节,从mysql 5.5开始,可支持4个字节UTF编码utf8mb4,一个字符最多能有4字节,所以能支持更多的字符集。
在这里插入图片描述
这个表格中包含了所有的 emoji 以及它所对应的 unicode编码,同时也有对应的 utf-8编码的实现。

从图中也可以看出 emoji 表情用 utf-8 表示时会占用 4个字节,这也就是为什么数据库用utf8无法存储emoji表情的原因了。

同样我们也可以在java代码中看看emoji占用几个字节长度:
在这里插入图片描述

ASCII码

上面介绍utf8mb4时有提过unicode,介绍它之前我们也需要先提一嘴我们的老朋友:ASCII码

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统。它主要用于显示现代英语。

这样我们就可以使用一个字节来表示现代英文,看起来非常不错,部分数据对应关系如下:
在这里插入图片描述
但这个只能显示的代表拉丁文,这显然是远远不够的。

Unicode

ASCII的局限在于只能显示26个基本拉丁字母、阿拉伯数字和英式标点符号,因此只能用于显示现代美国英语。

这时如果能有一种包含了世界上所有的文字的字符集,每一个地区的文字都在这个字符集中有唯一的二进制表示,这样便不会出现乱码问题了。所以Unicode也应运而生了。

Unicode,中文又称万国码、国际码、统一码、单一码,是计算机科学领域里的一项业界标准。它对世界上大部分的文字系统进行了整理、编码,使得电脑可以用更为简单的方式来呈现和处理文字。

平面

Unicode 首先承认了 ASCII 占用 0-127 整数资源的合法性,之后又一次占用了 128-65535 的整数资源,有了这么多的整数资源,我们就可以把世界各种文字的每一种字符分配一个整数来表示了。

之后,Unicode 联盟发现 65536 个整数也不够分配的,于是就索性一次性又把之后的 16 个 65536 的数字即 65536-1114111 的整数资源给占了,然后把多占的 16 个 65536 的段分别命名为 16 个平面,加上原来的 0-65535 平面,Unicode 总共有 17 个平面。比如第 1 平面就是 65536-131072。当然,到目前为止,还只分配了 7 个平面出去。
在这里插入图片描述
第0平面(Plane 0),是Unicode中的一个编码区段。编码从U+0000至U+FFFF,这个平面里面的字符是我们最常用到的。

65535 之后分配的字符大多数是 emoji 表情,比如 😺 是 128570(\uD83D\uDE3A)

这里推荐一个在线的编码转换网站:http://ctf.ssleye.com/cencode.html

表示范围

Unicode表示范围:U+0000 ~ U+10FFFF

也就大概是:U+0000~U+110000(加上1),也就是17个FFFF(65535)
差不多17*6w,大概有100w个码点可以用来映射字符
准确的值是 1114,112,差不多112w个码点
最新版本的Unicode含有136,690 个字符,离100w还很远。
Unicode 官方表示目前的码点已经够用,以后不再扩充

实现方式

Unicode的实现方式不同于编码方式。一个字符的Unicode编码是确定的。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。Unicode的实现方式称为Unicode转换格式(Unicode Transformation Format,简称为UTF)。

对于被Unicode收录的字符其编码是唯一且确定的。但是Unicode的实现方式(出于传输、存储、处理或向后兼容的考虑)却有不同的几种,其中最流行的是UTF-8、UTF-16、UCS2、UCS4/UTF-32等,细分的话还有大小端的区别。

对于我们Java而言,可以从char占用2字节来推断出使用的是UTF-16编码来存储

解决emoji截取的问题

言归正传,我们终究还是要解决开头提出的问题,如何正确的截取含有emoji的字符串?这里从UTF-16编码开始说起。

UTF-16

UTF-16 具体定义了 Unicode 字符在计算机中存取方法。UTF-16 用两个字节来表示 Unicode 转化格式,这个是定长的表示方法,不论什么字符都可以用两个字节表示,两个字节是 16 个 bit,所以叫 UTF-16。UTF-16 表示字符非常方便,每两个字节表示一个字符,这个在字符串操作时就大大简化了操作,这也是 Java 以 UTF-16 作为内存的字符存储格式的一个很重要的原因。

在基本多语言平面(码位范围U+0000-U+FFFF)内的码位UTF-16编码使用1个码元且其值与Unicode是相等的(不需要转换),这个就是我们正常的汉字,比如在辅助平面(码位范围U+10000-U+10FFFF)内的码位在UTF-16中被编码为一对16bit的码元(即32bit,4字节),称作代理对(surrogate pair)。组成代理对的两个码元前一个称为 前导代理(lead surrogates) 范围为0xD800-0xDBFF,后一个称为 后尾代理(trail surrogates) 范围为0xDC00-0xDFFF

surrogate

上面有提到surrogate,surrogate是代理的意思, 这个概念不是来自 Java 语言,而是来自 Unicode 编码方式之一 UTF-16。具体请见:UTF-16

简而言之,Java 语言内部的字符信息是使用 UTF-16 编码。因为char 这个类型是 16-bit 的。它可以有65536种取值,即65536个编号,每个编号可以代表1种字符。但是,Unicode 包含的字符已经远远超过65536个。那么编号大于65536的,还要用 16-bit 编码,该怎么办?于是Unicode 标准制定组想出的办法就是,从这65536个编号里,拿出2048个,规定它们是「Surrogates」,让它们两个为一组,来代表编号大于65536的那些字符。

更具体地,编号为 U+D800 至 U+DBFF 的规定为「High Surrogates」,共1024个。编号为 U+DC00至 U+DFFF 的规定为「Low Surrogates」,也是1024个。它们两两组合出现,就又可以多表示1048576种字符。

emoji截取异常原因

我们可以把emoji分离出来,如下:

🐳 -> \uD83D\uDC33

🐳 -> \uD83D\uDC33

🐠 -> \uD83D\uDC20

emoji肯定是大于65536的,所以这里就用「High Surrogates」和「Low Surrogates」两两组合的方式来呈现的。

由上面的UTF-16编码知识可以推断出,我们的emoji表情截取一个char后出现乱码的原因,是因为它是属于UTF-16编码辅助平面内的代理对,而我们如果截取时将代理对拆分开 就会出现异常的问题。

对于这种情况,我们可以通过Character类的静态方法isHighSurrogate和isLowSurrogate来判断,单个emoji的组合就是高位+低位,所以对于辅助平面内的代理对,做到整个移除或保留即可。

isHighSurrogate方法的源码如下:

public static final char MIN_HIGH_SURROGATE = '\uD800';public static final char MAX_HIGH_SURROGATE = '\uDBFF';public static boolean isHighSurrogate(char ch) {return ch >= MIN_HIGH_SURROGATE && ch < (MAX_HIGH_SURROGATE + 1);
}

问题解决

实现代码如下:

public static void main(String[] args) {// 用户昵称为:🐳🐳🐠,正常结果应该为:🐳***🐠String context = "\uD83D\uDC33\uD83D\uDC33\uD83D\uDC20";int realNameLength = realStringLength(context);String namePrefix = subString(context, 1, 0);String nameSuffix = subString(context, realNameLength - 1, 1);context = String.format("%s%s%s", namePrefix, "***", nameSuffix);System.out.println(context);
}/*** 包含emoji表情的subString方法** @param str 原有的str* @param len str长度* @param type type = 0 代表prefix,其他代表suffix*/
private static String subString(String str, int len, int type) {if (len < 0) {return str;}int count = 0;for (int i = 0; i < str.length(); i++) {if (count == len) {// type = 0 代表prefix,其他代表suffixif (type == 0) {return str.substring(0, i);}return str.substring(i);}char c = str.charAt(i);if (Character.isHighSurrogate(c) || Character.isLowSurrogate(c)) {i++;}count++;}return str;
}/*** 包含emoji表情的字符串实际长度** @param str 原有str* @return str实际长度*/
private static int realStringLength(String str) {int count = 0;for (int i = 0; i < str.length(); i++) {char c = str.charAt(i);if (Character.isHighSurrogate(c) || Character.isLowSurrogate(c)) {i++;}count++;}return count;
}

总结

这是一个字符集相关的知识引起的问题。
简单地说,是ASCII码只够显示现代美国英语,如果要包含全世界的所有文字的字符集,就不够了,这时候Unicode应运而生。
一开始Unicode占用了0-65535(即2^16)的整数资源,后来又把后面的16个65536 的数字即 65536-1114111 的整数资源给占了,然后把多占的 16 个 65536 的段分别命名为 16 个平面,加上原来的 0-65535 平面,Unicode 总共有 17 个平面。比如第 1 平面就是 65536-131072。
第0平面(Plane 0,基本多语言平面),是Unicode中的一个编码区段。编码从U+0000至U+FFFF, 这个平面里面的字符是我们最常用到的。65535 之后分配的字符大多数是 emoji 表情,比如 😺 是 128570(\uD83D\uDE3A)。
Java 语言内部的字符信息是使用 UTF-16 编码。因为char 这个类型是 16-bit 的。它可以有65536种取值,即65536个编号,每个编号可以代表1种字符。但是,Unicode 包含的字符已经远远超过65536个。那么编号大于65536的,还要用 16-bit 编码,该怎么办?于是Unicode 标准制定组想出的办法就是,从这65536个编号里,拿出2048个,规定它们是「Surrogates」,让它们两个为一组,来代表编号大于65536的那些字符。所以表情包占了2个字符,Java API也提供了Character.isHighSurrogate和Character.isLowSurrogate,可以用来判断字符串是不是表情包。如果是,处理时就要将2个连续的字符一同处理。

这篇关于emoji表情包字符分割问题引出的编码知识的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1035186

相关文章

MybatisGenerator文件生成不出对应文件的问题

《MybatisGenerator文件生成不出对应文件的问题》本文介绍了使用MybatisGenerator生成文件时遇到的问题及解决方法,主要步骤包括检查目标表是否存在、是否能连接到数据库、配置生成... 目录MyBATisGenerator 文件生成不出对应文件先在项目结构里引入“targetProje

C#使用HttpClient进行Post请求出现超时问题的解决及优化

《C#使用HttpClient进行Post请求出现超时问题的解决及优化》最近我的控制台程序发现有时候总是出现请求超时等问题,通常好几分钟最多只有3-4个请求,在使用apipost发现并发10个5分钟也... 目录优化结论单例HttpClient连接池耗尽和并发并发异步最终优化后优化结论我直接上优化结论吧,

Java内存泄漏问题的排查、优化与最佳实践

《Java内存泄漏问题的排查、优化与最佳实践》在Java开发中,内存泄漏是一个常见且令人头疼的问题,内存泄漏指的是程序在运行过程中,已经不再使用的对象没有被及时释放,从而导致内存占用不断增加,最终... 目录引言1. 什么是内存泄漏?常见的内存泄漏情况2. 如何排查 Java 中的内存泄漏?2.1 使用 J

Java 字符数组转字符串的常用方法

《Java字符数组转字符串的常用方法》文章总结了在Java中将字符数组转换为字符串的几种常用方法,包括使用String构造函数、String.valueOf()方法、StringBuilder以及A... 目录1. 使用String构造函数1.1 基本转换方法1.2 注意事项2. 使用String.valu

numpy求解线性代数相关问题

《numpy求解线性代数相关问题》本文主要介绍了numpy求解线性代数相关问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 在numpy中有numpy.array类型和numpy.mat类型,前者是数组类型,后者是矩阵类型。数组

解决systemctl reload nginx重启Nginx服务报错:Job for nginx.service invalid问题

《解决systemctlreloadnginx重启Nginx服务报错:Jobfornginx.serviceinvalid问题》文章描述了通过`systemctlstatusnginx.se... 目录systemctl reload nginx重启Nginx服务报错:Job for nginx.javas

Go语言使用Buffer实现高性能处理字节和字符

《Go语言使用Buffer实现高性能处理字节和字符》在Go中,bytes.Buffer是一个非常高效的类型,用于处理字节数据的读写操作,本文将详细介绍一下如何使用Buffer实现高性能处理字节和... 目录1. bytes.Buffer 的基本用法1.1. 创建和初始化 Buffer1.2. 使用 Writ

Redis缓存问题与缓存更新机制详解

《Redis缓存问题与缓存更新机制详解》本文主要介绍了缓存问题及其解决方案,包括缓存穿透、缓存击穿、缓存雪崩等问题的成因以及相应的预防和解决方法,同时,还详细探讨了缓存更新机制,包括不同情况下的缓存更... 目录一、缓存问题1.1 缓存穿透1.1.1 问题来源1.1.2 解决方案1.2 缓存击穿1.2.1

使用Python将长图片分割为若干张小图片

《使用Python将长图片分割为若干张小图片》这篇文章主要为大家详细介绍了如何使用Python将长图片分割为若干张小图片,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. python需求的任务2. Python代码的实现3. 代码修改的位置4. 运行结果1. Python需求

vue解决子组件样式覆盖问题scoped deep

《vue解决子组件样式覆盖问题scopeddeep》文章主要介绍了在Vue项目中处理全局样式和局部样式的方法,包括使用scoped属性和深度选择器(/deep/)来覆盖子组件的样式,作者建议所有组件... 目录前言scoped分析deep分析使用总结所有组件必须加scoped父组件覆盖子组件使用deep前言