本文主要介紹了UTF8的一些基本概念,簡要介紹了mysql中 utf8 utf8mb3 utf8mb4 的區別;而後爲介紹Java對Unicode編碼的支持,引入了一些編碼的基本概念,包括code point, code unit等,並介紹了Java提供的經常使用的支持Unicode編碼的方法;最後給出了過濾UTF8mb4的方案html
UTF-8(8-bit Unicode Transformation Format)是一種針對Unicode的可變長度字符編碼,也是一種前綴碼。它能夠用來表示Unicode標準中的任何字符,且其編碼中的第一個字節仍與ASCII兼容,這使得原來處理ASCII字符的軟件無須或只須作少部分修改,便可繼續使用。所以,它逐漸成爲電子郵件、網頁及其餘存儲或發送文字的應用中,優先採用的編碼。java
UTF-8使用一至四個字節爲每一個字符編碼(2003年11月UTF-8被RFC 3629從新規範,只能使用原來Unicode定義的區域,U+0000到U+10FFFF,也就是說最多四個字節):mysql
128個US-ASCII字符只需一個字節編碼(Unicode範圍由U+0000至U+007F)。sql
帶有附加符號的拉丁文、希臘文、西裏爾字母、亞美尼亞語、希伯來文、阿拉伯文、敘利亞文及它拿字母則須要兩個字節編碼(Unicode範圍由U+0080至U+07FF)。數據庫
其餘基本多文種平面(BMP, Basic Multilingual Plane)中的字符(這包含了大部分經常使用字,例如CJVK經常使用字字符集 —— Chinese, Japanese, Vietnam, Korean)使用三個字節編碼(Unicode範圍由U+0800至U+FFFF)。api
其餘使用極少的Unicode 輔助平面(Supplementary Multilingual Plane)的字符使用四字節編碼(Unicode範圍由U+10000至U+10FFFF,主要包括不經常使用的CJK字符, 數學符號, emoji表情等)。oracle
utf-8編碼方式
app
unicode code point table
ui
參考與擴展:
維基百科 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編碼
utf8編碼是unicode編碼的一種實現,能夠簡單的理解爲unicode編碼定義一串數字來一一對應咱們用到的字符,utf8定義瞭如何將unicode定義的這串數字保存到內存中。 另外須要強調的是utf8是一種變長的編碼規範。
unicode 的範圍 U+0000 - U+10FFFF。
參考與擴展
維基百科 Unicode https://en.wikipedia.org/wiki/Unicode
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經常使用的編碼方式。
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
最初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的專家討論了不少方法想要解決這個問題,其中包括:
前文提到了UTF16用兩個編碼單元來表示超過U+FFFF的1,048,576 (1024*1024)個字符,Java中與之對應的概念就是"代理對(surrogate pair)"。
下面介紹Java中幾個經常使用的code point(int)和char的轉換方法
下面是一個簡單的例子:
@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字符不會自動計算爲兩個代碼單元,從源碼(見附錄)中能夠看到
@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
在理解了前面的概念後,我想再過濾掉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; }