共计 6430 个字符,预计需要花费 17 分钟才能阅读完成。
前言
之前也分享过很多工作中踩坑的教训:
- 一个线上问题的思考:Eureka 注册核心集群如何实现客户端申请负载及故障转移?
- 【原创】教训分享:一个 Content-Length 引发的血案(almost….)
明天再来分享工作中一个实在的案例:
商品评估列表页,显示每条用户的评估详情,为了爱护用户隐衷,要求显示用户昵称时只能显示第一位和最初一位,其余的用※代替。
例如输出:????????????,输入:????*????
看似一个平铺直叙的需要,我也没有太在意。服务端将用户的评论信息存储到 db
中,评估列表接口就是将数据库中该商品的评论信息展现进去,非凡解决下评论人的昵称就能够了。
然而!! 测试同学发现用户昵称蕴含 emoji 表情
时就会出问题,切割的数据会有 问号显示!!
模仿的示例代码如下:
输入:
看到这个输入,我真的是一脸懵逼,这齐全不是我想要的后果呀!!!
这三个鱼可算是难倒我了,难道只能给测试说 emoji 太非凡 不予解决?而后 撒个娇 蒙混过关?
思考了好久,我还是决定要正视这个问题并解决掉它!(毕竟我还是那个不畏艰难的小机灵鬼????)
PS:本文很大水平是受到之前公司一位共事 unicode 分享的启发,在这里向我的这位老师致敬!上面的内容会一步步剖析这个问题的产生以及最终的解决方案。
概念常识
要解决这些问题,就必须要铺垫一些基础知识,大家等不及看解决方案 能够拉到文章最初的代码示例。
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
占用几个字节长度:
咱们也能够看到 String.getBytes()
,默认是utf-8
编码的:
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(uD83DuDE3A)
这里举荐一个在线的编码转换网站:http://ctf.ssleye.com/cencode…
示意范畴
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
编码来存储
对于各种编码问题举荐一篇好文:深入分析 Java 中的中文编码问题
判断是否蕴含中文
下面大略理解了 Unicode
的含意及用处,那么理解这个玩意有什么理论作用呢?
咱们再来看一个小的需要,比方:如何判断一个字符串中蕴含中文?
置信大家也遇到过这种需要吧,个别咱们都会去百度一通,肯定都能找到一个判断是否蕴含中文的正则表达式,而后满心欢喜解决了问题。
凑巧咱们零碎中也有这么一个正则判断,是架构组的共事封装好的,一起来看下:
显然,这里是通过 Unicode
区间去判断的,有没有问题呢?
这里的区间是用的中日韩对立表意文字,然而这个是 1993 年的版本,蕴含了大部分咱们罕用的中文,共有 20902 个字,看到前面补充的版本,还增加了很多字,由此可想像咱们当初应用的判断形式必定会漏掉后增加的字:
咱们用 2000 年减少的中日韩对立表意文字扩大区 A 来举例测试一下:
这里加了很多生僻字,甚至都没有我意识的,咱们用第二排的数据来做一个验证:
看到这里是不是很诧异?并高呼你们这里写了一个bug
,哈哈。
其实这里并不能说咱们的正则判断有bug
,这个须要看咱们的需要是否精准到所有的生僻词都得辨认到。依据用户的应用习惯,输出这些生僻字的概率不是很高,所以这个正则并没有小伙伴反馈有问题。
解决 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
分离出来,如下:
???? -> uD83DuDC33
???? -> uD83DuDC33
???? -> uD83DuDC20
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);
}
这个判断其实就是下面说的 「High Surrogates」
的断定形式,咱们能够转换一下:
U+D800 <= ch <= U+DBFF
同理,isLowSurrogate
办法的断定形式也是一样的:
U+DC00 <= ch <= U+DFFF
问题解决
还是先运行一下代码,看看成果:
具体实现代码如下:
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,其余代表 suffix
if (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;
}
彩蛋:认领属于你的 emoji
emoji
远远不止于此,unicode
旗下还能够反对对 emoji
进行捐献的,当然这个 emoji
会以捐赠者的名义去命名的。如下是现有的捐献列表:
看到第一个就是 elastic.co 捐献的,而且点击链接能够间接进入他们官网。第二个捐献列表中还有一个是我共事捐献的,哈哈,很有意思。
如果想本人捐献也能够间接进入到 [emoji 捐献网站](https://www.unicode.org/conso…
) 去填写个人信息,一共有三个档位,捐献后这个列表就会显示由你定义的 emoji
信息了,几乎太酷了????:
总结
一个小小的 emoji
真是学识无穷,因为篇幅的问题我这里还省略了很多货色,比方 UTF-8
和UTF-16
两种编码模式并没有深刻解说,这外面又会牵扯到很多内容。
我心愿这篇文章可能做到一个 抛砖引玉 的作用,激发小伙伴们一起去探索更多的神秘。
参考
- 维基百科 Unicode:https://zh.wikipedia.org/wiki…
- 维基百科 Unicode 字符立体映射:https://zh.wikipedia.org/wiki…
- 不要小看小小的 emoji 表情:https://juejin.im/post/684490…
- 谈谈字符编码:Unicode、UTF-8 和 char[]:https://luan.ma/post/characte…
- 字符截断引发的 emoji 表情乱码问题:https://superxlcr.github.io/2…
- emoji 捐献列表:https://www.unicode.org/conso…
欢送关注我的公众号,一起交流学习: