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编码方式
utf-8编码方式

unicode code point table
unicode

参考与扩展
维基百科 UTF-8 https://en.wikipedia.org/wiki/UTF-8, 中文版 https://zh.wikipedia.org/wiki/UTF-8
维基百科 Plane_(Unicode) https://en.wikipedia.org/wiki/Plane_%28Unicode%29
维基百科 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 / utf8mb3mysql utf8mb4
max bit34
范围基本多文种平面 + US-ASCII辅助平面(Supplementary) + 基本多文种平面 + US-ASCII
unicode范围U+0000 - U+FFFFU+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-8编码方式
UTF-16编码方式
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位
Unicode定义的6种BOM位

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

UTFs的属性归纳
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;
    }

转载于:https://www.cnblogs.com/chrischennx/p/6623610.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值