java mysql utf8mb4_Java Unicode编码 及 Mysql utf8 utf8mb3 utf8mb4 的区别与utf8mb4的过滤

内容简介

本文主要介绍了UTF8的一些基本概念,简要介绍了mysql中 utf8 utf8mb3 utf8mb4 的区别;然后为介绍Java对Unicode编码的支持,引入了一些编码的基本概念,包括code point, code unit等,并介绍了Java提供的常用的支持Unicode编码的方法;最后给出了过滤UTF8mb4的方案

UTF-8简介

UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,也是一种前缀码。它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的软件无须或只须做少部分修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或发送文字的应用中,优先采用的编码。

UTF-8使用一至四个字节为每个字符编码(2003年11月UTF-8被RFC 3629重新规范,只能使用原来Unicode定义的区域,U+0000到U+10FFFF,也就是说最多四个字节):

128个US-ASCII字符只需一个字节编码(Unicode范围由U+0000至U+007F)。

带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要两个字节编码(Unicode范围由U+0080至U+07FF)。

其他基本多文种平面(BMP, Basic Multilingual Plane)中的字符(这包含了大部分常用字,例如CJVK常用字字符集 —— Chinese, Japanese, Vietnam, Korean)使用三个字节编码(Unicode范围由U+0800至U+FFFF)。

其他使用极少的Unicode 辅助平面(Supplementary Multilingual Plane)的字符使用四字节编码(Unicode范围由U+10000至U+10FFFF,主要包括不常用的CJK字符, 数学符号, emoji表情等)。

utf-8编码方式

f899a90817a7563f85cf404fab77cb13.png

unicode code point table

8ee2cccf800b0cf8cbf26851fc3f380c.png

UTF-8与Unicode的关系

utf8编码是unicode编码的一种实现,可以简单的理解为unicode编码定义一串数字来一一对应我们用到的字符,utf8定义了如何将unicode定义的这串数字保存到内存中。 另外需要强调的是utf8是一种变长的编码规范。

unicode 的范围 U+0000 - U+10FFFF。

Mysql中的 UTF-8、UTF8mb3, UTF8mb4

utf8mb4, MySQL在5.5.3之后增加了这个utf8mb4的编码,mb4就是most bytes 4的意思,专门用来兼容四字节的unicode字符。

mysql中的utf8,就是最大3字节的unicode字符,也就是mysql中的utf8mb3.

表示范围:

说明

mysql utf8 / utf8mb3

mysql utf8mb4

max bit

3

4

范围

基本多文种平面 + US-ASCII

辅助平面(Supplementary) + 基本多文种平面 + US-ASCII

unicode范围

U+0000 - U+FFFF

U+0000 - U+10FFFFF

常见字符

英文字母,CJK大部分常用字等

CJK非常用字,数学符号,emoji表情等

那么问题来了,如果用了utf8mb3编码的mysql数据库,在插入一些4字节长的字符时就会报错(形如:"java.sql.SQLException: Incorrect string value: '\xF0\x9F\x94\x91\xE6\x9D...' for column 'core_data' at row 1" 的错误),后文会介绍如何在Java中过滤掉这些字符。

要在Java中过滤Mysql的utf8mb4,必须弄清Java是如何支持Unicode编码,接下来徐徐展开......

编码简介

下面先介绍几个概念:character(字符), character set(字符集), coded character set(字符编码集), code point(代码点), code space(代码空间),character encoding scheme(字符编码方案),code unit(编码单元),和3种Unicode常用的编码方式。

character——字符,'a', '€', '中' 等, 都是一个字符

character set——字符集,字符的集合

coded character set——字符编码集,为每一个字符指定一个唯一的数字用来表示这个字符,这些数字组成的集合就是字符编**码集合,Unicode就是一个字符编码集

code point——代码点,是一个数字,用来表示字符集中的一个字符,也就是字符编码集中的一个数,例如 Unicode 编码中, 'A'的code point就是65(在Unicode中通常写作 U+0041)

code space——代码空间,就是一个编码集中,code point的范围, 例如 Unicode 编码的 code space 就是 0x0000 - 0x10FFFF

character encoding scheme——字符编码方案,它定义了将字符用一个或多个固定长度的代码单元的方案,如前文提到的"utf-8编码方式"就是一个字符编码方案,其它的还有UTF16,UTF32,GBK等等

code unit——编码单元,就是编码方案中固定长度的最小编码单元,如UTF8的编码单元是1bit,UTF16是2bit,UTF32是4bit,

Unicode常用的三种编码方式 UTF-8, UTF-16, UTF-32, 下面以辅助平面中的字符'🔑' 为例做一个简要的介绍, 它的code point为128273(0x1F511):

utf8,编码单元为8bit,使用1-4个编码单元来表示Unicode中的字符,辅助平面中的字符在utf8中需要用4字节表示,对照前面的utf-8编码方案中4字节的编码格式, 从高到低依次为:11110xxx10xxxxxx10xxxxxx10xxxxxx, 所以其编码是编码是 '11110000 10011111 10010100 10010001',注意并不是 0x1F511的二进制表示,不要混淆

utf16, 编码单元是16bit,用1-2个编码单元来表示Unicode中的字符,U+0000-U+FFFF(BMP)用一个编码单元表示,0x10000-0x10FFFF(SMP)用两个编码单元(high-surrogates和low-surrogates)表示,high-surrogates范围U+D800-U+DBFF,low-surrogates范围U+DC00-U+DFFF,编码方式见下文图片,编码结果为'11011000 00111101 11011101 00010001'。在Unicode编码中U+D800-U+DFFF是专门为UTF16保留的区间,没有分配其它字符,所以不用担心一个code point有两个含义的问题。

utf32,编码半圆是32bit,可以只用一个编码单元来表示全部的Unicode字符,其编码就是 code point的值,也就是 '00000000 00000001 11110101 00010001'。

UTF-8编码方式

f899a90817a7563f85cf404fab77cb13.png

UTF-16编码方式

ff10ebe7bdffe95921a13058b058f227.png

打印编码的code:

@Test

public void printCharacterCode() {

String s = "\uD83D\uDD11"; //字符'🔑'

log.info("UTF8: {}", bytesToBits(s.getBytes(Charset.forName("utf-8"))));

log.info("UTF16: {}", bytesToBits(s.getBytes(Charset.forName("utf-16"))));

log.info("UTF32: {}", bytesToBits(s.getBytes(Charset.forName("utf-32"))));

}

public static String byteToBit(byte b) {

return ""

+ (byte) ((b >> 7) & 0x1) + (byte) ((b >> 6) & 0x1)

+ (byte) ((b >> 5) & 0x1) + (byte) ((b >> 4) & 0x1)

+ (byte) ((b >> 3) & 0x1) + (byte) ((b >> 2) & 0x1)

+ (byte) ((b >> 1) & 0x1) + (byte) ((b >> 0) & 0x1);

}

public static String bytesToBits(byte[] bytes) {

String s = "";

for (byte b : bytes) {

s += byteToBit(b) + " ";

}

return s;

}

使用上面的代码打印结果如下:

UTF8: 11110000 10011111 10010100 10010001

UTF16: 11111110 11111111 11011000 00111101 11011101 00010001

UTF32: 00000000 00000001 11110101 00010001

可以看到utf-16的结果并非我们期待的'11011000 00111101 11011101 00010001', 前面多了一个编码单元 'FEFF', 这个是这个是Unicode编码中的 BOM(byte order mark)位,用来表示byte(注意不是bit)的顺序,BOM是可选的,如果用那么它必须出现在字符串的开始(在其它编码中BOM不会出现在字符串开始,所以可以用来识别字符串是否Unicode编码)。

为什么要用BOM位?为了标识编码单元的字节序,例如:“奎”的Unicode编码是594E,“乙”的Unicode编码是4E59,如果我们收到UTF-16字节流“594E”,那么这是“奎”还是“乙”? 如果字符串的字节码是 'FEFF 4E59',那么则表示大端在左(big-endian),这个字是“乙”。

Unicode定义的6种BOM位

f8679ec5d18f529bd8324b2cb480d178.png

BOM位是可以缺省的,缺省时默认大端在左。

UTFs的属性归纳

d812ce3ee9fcab4d9bc54ecd1418e96a.png

Java与Unicode

最初Unicode的编码数量并没有超过65,535 (0xFFFF),早期Java版本中使用16bit的char表示当时全部的Unicode字符。后来Unicode字符集扩展到了1,114,111 (0x10FFFF)(在Unicode标准2.0用引入了辅助编码平面SMP,在3.1首次为SMP的部分编码分配了字符), JAVA中的char已经不足以表示Unicode的全部编码(需要32bit),JSR-204的专家讨论了很多方法想要解决这个问题,其中包括:

设计一种新的字符类型char32来替换原有的char

用int来表示code point,同时保留,并为String和StringBuffer等增加兼容char和int表示的api

...

最后处于内存占用和兼容性等方面的考虑,采用了如下方法:

在底层api中用int来表示code point,比如在Character类中

所有的字符串都char表示,并采用utf16的格式来表示,并提倡在高层api中使用这种方式

提供便于在int(code point)和char之间转换的方法,用于必要时候两者的转换

前文提到了UTF16用两个编码单元来表示超过U+FFFF的1,048,576 (1024*1024)个字符,Java中与之对应的概念就是"代理对(surrogate pair)"。

下面介绍Java中几个常用的code point(int)和char的转换方法

Character.toCodePoint(char high, char low),return int,将两个UTF16的char(两个UTF16代码单元)转换为code point

Character.toChars(int codePoint), return char[],将code point转换为一个或两个UTF16代码单元

isSupplementaryCodePoint(int codePoint), 判断一个code point是否SMP(Unicode中超过U+FFFF)的字符

Character.isSurrogate(char ch), 判断一个char是否为UTF16超过U+FFFF的两代码单元的字符的一个代码单元

Character.isHighSurrogate(char ch), 判断是否UTF16中两单元字符的高位单元

Character.isLowSurrogate(char ch), 判断是否UTF16中两单元字符的低位单元

Stirng提供的length(), 这是一个比较常用的方法,但是它的实际含义是UTF16代码单元的个数,也就是说如果字符串中包含了两代码单元的字符,那么length的值比实际的字符个数要多

String提供的codePointCount(), 这个是返回的代码点的个数,对于不包含两代码单元的字符时,其值等于length的值,包含时,其值为字符的个数,小于length的值

StringBuilder和StringBuffer主要提供的都是string和char的append方法,但是也提供了一个可以通过codePoint添加字符的方法 appendCodePoint(int codePoint)

下面是一个简单的例子:

@Test

public void testConverterOfCodePointAndChar() {

String s = "a中\uD83D\uDD11a中";

for (int i = 0; i < s.codePointCount(0, s.length()); i++) {

int codePoint = s.codePointAt(i);

log.info("code point at {}: {},\t isSupplementaryCodePoint:{}", i, codePoint, Character.isSupplementaryCodePoint(codePoint));

}

for (int i = 0; i < s.length(); i++) {

char c = s.charAt(i);

log.info("char at {}: {},\t isSurrogate:{},\t isHighSurrogate:{},\t isLowSurrogate:{}, ", i, c, Character.isSurrogate(c), Character.isHighSurrogate(c), Character.isLowSurrogate(c));

}

}

输出结果为:

code point at 0: 97, isSupplementaryCodePoint:false

code point at 1: 20013, isSupplementaryCodePoint:false

code point at 2: 128273, isSupplementaryCodePoint:true

code point at 3: 56593, isSupplementaryCodePoint:false

code point at 4: 97, isSupplementaryCodePoint:false

char at 0: a, isSurrogate:false, isHighSurrogate:false, isLowSurrogate:false

char at 1: 中, isSurrogate:false, isHighSurrogate:false, isLowSurrogate:false

char at 2: ?, isSurrogate:true, isHighSurrogate:true, isLowSurrogate:false

char at 3: ?, isSurrogate:true, isHighSurrogate:false, isLowSurrogate:true

char at 4: a, isSurrogate:false, isHighSurrogate:false, isLowSurrogate:false

char at 5: 中, isSurrogate:false, isHighSurrogate:false, isLowSurrogate:false

上面的例子中我们看到一个奇怪的现象,codePointCount获取的字符的个数是对的,但是通过codePointAt去获取时,遇到SMP字符不会自动计算为两个代码单元,从源码(见附录)中可以看到

codePointCount中是通过判断是通过length的值减去2代码单元的个数得到

codePointAt 是通过判断当前代码单元是否UTF16高位单元,当是高位单元时会自动获取低位单元的值,得到完整的code point,但是获取到低位单元时不会做处理

所以要正确的遍历一个有2代码单元的字符时,需要自己做处理:

@Test

public void testIterateCodePoint() {

String s = "a中\uD83D\uDD11a中";

for (int i = 0; i < s.length(); i++) {

int codePoint = s.codePointAt(i);

log.info("code point at {}: {},\t isSupplementaryCodePoint:{}", i, codePoint, Character.isSupplementaryCodePoint(codePoint));

if (Character.isSupplementaryCodePoint(codePoint)) i++;

}

}

输出结果为:

code point at 0: 97, isSupplementaryCodePoint:false

code point at 1: 20013, isSupplementaryCodePoint:false

code point at 2: 128273, isSupplementaryCodePoint:true

code point at 4: 97, isSupplementaryCodePoint:false

code point at 5: 20013, isSupplementaryCodePoint:false

Java过滤4字长UTF-8编码字符

在理解了前面的概念后,我想再过滤掉4字长的UTF-8字符已经不难了吧。

4字长的UTF-8字符就是Unicode SMP(辅助平面)中的字符, 也就是Unicode编码大于U+FFFF的字符, 所以我们只需要获取字符串中各个字符的code point,当code point 大于FFFF时(或者直接使用Character.isSupplementaryCodePoint来判断),过滤掉即可,示例代码如下:

@Test

public void filterUtf8mb4Test() {

String s = "a中\uD83D\uDD11a中";

log.info(filterUtf8mb4(s));

}

public static String filterUtf8mb4(String str) {

final int LAST_BMP = 0xFFFF;

StringBuilder sb = new StringBuilder(str.length());

for (int i = 0; i < str.length(); i++) {

int codePoint = str.codePointAt(i);

if (codePoint < LAST_BMP) {

sb.appendCodePoint(codePoint);

} else {

i++;

}

}

return sb.toString();

}

输出结果为:

a中a中

附录

String的 codePointCount 和 codePointAt 源码:

public int codePointCount(int beginIndex, int endIndex) {

if (beginIndex < 0 || endIndex > value.length || beginIndex > endIndex) {

throw new IndexOutOfBoundsException();

}

return Character.codePointCountImpl(value, beginIndex, endIndex - beginIndex);

}

public int codePointAt(int index) {

if ((index < 0) || (index >= value.length)) {

throw new StringIndexOutOfBoundsException(index);

}

return Character.codePointAtImpl(value, index, value.length);

}

它们调用的Character的 codePointCountImpl 和 codePointAtImpl 的源码:

static int codePointCountImpl(char[] a, int offset, int count) {

int endIndex = offset + count;

int n = count;

for (int i = offset; i < endIndex; ) {

if (isHighSurrogate(a[i++]) && i < endIndex &&

isLowSurrogate(a[i])) {

n--;

i++;

}

}

return n;

}

static int codePointAtImpl(char[] a, int index, int limit) {

char c1 = a[index];

if (isHighSurrogate(c1) && ++index < limit) {

char c2 = a[index];

if (isLowSurrogate(c2)) {

return toCodePoint(c1, c2);

}

}

return c1;

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值