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编码方式
unicode code point table
参考与扩展:
维基百科 UTF-8 https://en.wikipedia.org/wiki/UTF-8, 中文版 https://zh.wikipedia.org/wiki/UTF-8
维基百科 Plane_(Unicode) https://en.wikipedia.org/wiki/Plane_(Unicode)
维基百科 CJK characters https://en.wikipedia.org/wiki/CJK_characters
维基百科 Emoji https://en.wikipedia.org/wiki/Emoji
UTF-8与Unicode的关系
utf8编码是unicode编码的一种实现,可以简单的理解为unicode编码定义一串数字来一一对应我们用到的字符,utf8定义了如何将unicode定义的这串数字保存到内存中。 另外需要强调的是utf8是一种变长的编码规范。
unicode 的范围 U+0000 - U+10FFFF。
参考与扩展
维基百科 Unicode https://en.wikipedia.org/wiki/Unicode
Mysql中的 UTF-8、UTF8mb3, UTF8mb4
utf8mb4, MySQL在5.5.3之后增加了这个utf8mb4的编码,mb4就是most bytes 4的意思,专门用来兼容四字节的unicode字符。
mysql中的utf8,就是最大3字节的unicode字符,也就是mysql中的utf8mb3.
参考
mysql-charset-unicode-utf8mb3 https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb3.html and https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8.html
mysql-charset-unicode-utf8mb4 https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html
表示范围:
说明 | 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字节的编码格式, 从高到低依次为:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx, 所以其编码是编码是 '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编码方式
UTF-16编码方式
打印编码的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位
BOM位是可以缺省的,缺省时默认大端在左。
UTFs的属性归纳
参考与扩展
Supplementary Characters in the Java Platform http://www.oracle.com/us/technologies/java/supplementary-142654.html
Unicode surrogate programming with the Java language https://www.ibm.com/developerworks/library/j-unicode/
微机百科 UTF16 https://zh.wikipedia.org/wiki/UTF-16
维基百科 code-point https://en.wikipedia.org/wiki/Code_point
D000-DFFF编码表 http://jicheng.tw/hanzi/unicode.html?s=D000&e=DFFF
utf bom http://unicode.org/faq/utf_bom.html
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;
}