mysql字符集為utf8時incorrect string value異常

先說編碼

1.為什么需要編碼

  • 計算機是英語國家做出來的玩意兒,所以它只能認識英文字母符號,阿拉伯數字等,其他所有的語言要想在計算機中顯示就必須進行編碼,翻譯。而計算機中表示數據信息的最小單元為一個字節,即8個bit,可以表示0~255個字符,世界上的各種語言千奇百怪,需要多個字節來表示,多個字節表示的數據要存儲在計算機中或者要在網絡間傳輸必須轉化為計算機認識的byte,如何轉換,那就是編碼。

2.常見的編碼

  • ascii碼

只占用一個字節,第一位為0,用低7位來編碼。

  • ISO-88591系列(ISO-88591-1 到ISO-88591-15)

這玩意兒也只占用一個字節,歐洲一些國家的字符用ascii沒法表示出來,所以把ascii編碼的首位也用來編碼。

  • GB2312

可以表示漢字的編碼,兩個字節(兼容ascii碼單字節形式),能表示6000多個漢字(區位碼,高位字節存當前漢字在哪個區,低位字節存在區中第幾位),相對算是挺少的。。。

其實在GB2312編碼里,并不是所有的字符都會用兩個字節來表示的。為了能清晰說明這個這個問題,我用二進制編碼來解釋一下。 首先,ASCII編碼雖然說是用一個字節來表示字符,但是它其實只用了后7位,第1位永遠是0。它的編碼范圍,從00000000到01111111,都是以0開頭的。 而GB2312編碼,就是在ASCII編碼的基礎上進行擴充的,它規定了:ASCII的字符完整地包含在GB2312里,編碼不變,仍然是以0開頭,用一個字節來表示一個字符;對于ASCII沒有的字符,就用1開頭來區分,用兩個字節合起來表示一個字符。 這樣,在解碼的時候,遇到字節是以0開頭的,就知道這一個字節就表示了一個字符;遇到字節是以1開頭的,就知道要加上下一個字節合起來表示一個字符。這樣就在GB2312中既把ASCII的字符包含了進來,又能將它們區分出來,能達到兼容的效果了。

  • GBK

也是兩個字節,但是可以表示的漢字有幾萬個,完全是在gb2312的基礎上進行擴展,完全兼容GB2312編碼,所以基本可以忘記有GB2312這個編碼,直接使用GBK替代他就好。
tips:其實gbk和GB2312也算是變長編碼,根據碼表查詢到碼位置大小來進行判斷用一個字節還是雙字節。

=====================
以下兩個unicode編碼的實現..

  • UTF-16

tips:最最最高效的編碼

他的編碼理念是常見的普通字符都用兩個字節存儲,而且不需要查碼表什么的,因為unicode規范里,世界上任何一個符號,字符都有一個唯一編碼對應,所以utf-16直接將對應的字符的unicode編碼分別放在兩個字節里即可,單字節就能表示的字符它以高位補0方式存儲。(以不同的處理器可能會以高位在前或者高位在后的方式解析,所以編碼解碼時需要指定到底是高位在前還是低位在前)

對于UCS-4輔助平面內的字符,采用四字節存儲:
Unicode碼位值為2AEAB,減去0x10000得到1AEAB(二進制值為0001 1010 1110 1010 1011),前10位加上D800得到D86B,后10位加上DC00得到DEAB。于是該字的UTF-16編碼值為D86BDEAB(該值為大端表示,小端為6BD8ABDE)。

  • UTF-8

任何一個程序員都知道的屌屌的編碼,它對utf-16編碼的優化:
1.變長編碼,高位為0,表示它是一個ascii編碼字符(ascii編碼本身就是首位為0,所以utf-8完全兼容ascii編碼),大于一個字節的編碼,會在首位以連續的1的個數來標識是幾個字節。后面字節的前兩位一律設為10。
2.變長帶來的好處:相對節約空間,安全(utf-16是每兩個挨著的字節表示一個字符,如果在網絡上傳輸時丟失了某個字節,那么該字節后面的所有編碼將會全部亂掉,而utf-8編碼將不會應用后續的編碼)

uftf-8編碼示意:

Unicode符號范圍 | UTF-8編碼方式
(十六進制) | (二進制)
--------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
image.png

3.編碼選擇

  • 首先,中文不能使用ascii或者iso系列,單字節編碼不可以表示中文
  • 其次,gbk和gb2312必須選gbk,不用多說(現在已經流行GB18030了)
  • utf-8和utf-16之間:java虛擬機使用utf-16進行編碼,追求編碼解碼速度(字節固定好查找),字節只在機器本地的磁盤和內存之間流動,不走網絡,所以也不會丟失。 在web開發中utf-8是最佳最佳選擇,空間小,容錯率高~~~

4.深入javaweb書上提到的關鍵點

  • 看一段字符到底會占多少內存,不是看string.length有多長,而是要看其用的什么編碼,(作者提到他在做項目時對cookie做過各種壓縮,但是最終并木有任何成效)

  • java的string使用的編碼是unicode,但是,當string存在于內存中時(也就是當程序運行時、你在代碼中用string類型的引用對它進行操作時、也就是string沒有被存在文件中且也沒有在網絡中傳輸(序列化)時),是“只有編碼而沒有編碼格式的”,所以java程序中的任何String對象,說它是gbk還是utf-8都是錯的,gbk和utf-8是編碼格式而不是編碼,String在內存中不需要“編碼格式”(記住編碼格式是在存文件或序列化的時候使用的), 它只是一個unicode的字符串而已

作者:溫悅
鏈接:https://www.zhihu.com/question/20361462/answer/14899233
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

5.書中的其他知識點(與本文的東西無關)

http請求的編碼:

  • 首先,urlPath路徑上的字符一般是通過utf-8編碼,而querystring中則是使用gbk編碼(為什么url中的中文編碼后會有一大堆%,因為url編碼規范規定轉換為16進制表示后需要在每個16進制位前加上%),<b>瀏覽器對url中path和query的編碼不一樣</b>
  • tomcat對path的編碼定義<Connector URIEncoding="UTF-8"/>這里不定義將默認采用iso編碼。
  • 對于query的編碼解碼,是在后端代碼中調用request.getParameter("xx")時進行的。解碼時會先查看http header的contenttype重定義的charset(需要在tomcat重配置<Connector URIEncoding="UTF-8" useBodyEncodingForURI="true"/>)
    tips:<b>在平時的開發中,盡量避免在uri或者path中傳中文,很容易亂碼,因為不同的瀏覽器編碼規則不一樣,后端開發者沒辦法完全掌控</b>
  • 對于header的編碼解碼,沒地方可以配置,所以http header不支持傳入中文,一定要傳入中文需要先編碼。解碼觸發時機為request.getHeader("xx"),解碼方式固定以:iso
  • 對于http postbody內容,瀏覽器端會先根據contentType設置的charset來進行編碼,服務端接收到數據也將會以contentType中的charset進行解碼。

utf-8再稍微多說一點

utf-8:https://en.wikipedia.org/wiki/UTF-8#Description

從上個小節得出結論,utf-8這種變長的表示方式其實可以表示8個字節以內的大小的編碼(因為它是以首字母的連續1的個數來判斷是幾個字節的),其實utf-8在目前的最大可表示字節數為4.對應如下:

ddddd.png

數據庫的字符集utf8與utf8mb4

使用show variables like ‘%character%’;可以查看mysql當前的字符集。

  • utf8:標準的 UTF-8 字符集編碼是可以用 1~4 個字節去編碼21位字符,這幾乎包含了是世界上所有能看見的語言了。然而在MySQL里實現的utf8最長使用3個字節,也就是只支持到了 Unicode 中的U+0000至U+FFFF),包含了控制符、拉丁文,中、日、韓等絕大多數國際字符。重要的事情說三遍:<b>特智能表示3個字節以內的編碼字符</b>,<b>特智能表示3個字節以內的編碼字符</b>,<b>特智能表示3個字節以內的編碼字符</b>

  • utf8mb4:新版本的mysql才支持,簡單說 utf8mb4 是 utf8 的超集并完全兼容utf8,能夠用四個字節存儲更多的字符。(當然這樣空間會比較浪費一點)

如果當前db的字符集為utf8,插入的數據為4個字節編碼時,將會拋出:incorrect string value異常
nested exception is org.apache.ibatis.exceptions.PersistenceException: \n### Error updating database. Cause: ERR-CODE: [TDDL-4601][ERR_EXECUTOR] Incorrect string value: '\\xF2\\xBC\\xAF\\xBA\\xEF\\xBF...' for column 'config_value' at row 1 More: [http:\/\/middleware.alibaba-inc.com\/faq\/faqByFaqCode.html?faqCode=TDDL-4601]\n### The error may involve com.alibaba.cornerstone.dao.output.OutputAppConfigDAO.addBatch-Inline\n### The error occurred while setting parameters\n### SQL: insert into output_appconfig ( app_name, component_id, config_value, config_type, finished ) values ( ?, ?, ?, ?, ? ) , ( ?, ?, ?, ?, ? ) , ( ?, ?, ?, ?, ? )\n### Cause: ERR-CODE: [TDDL-4601][ERR_EXECUTOR] Incorrect string value: '\\xF2\\xBC\\xAF\\xBA\\xEF\\xBF...' for column 'config_value' at row 1

插入數據庫的數據是一堆mysql建表語句,為什么會出現4個字節編碼的字符

  • 寫了個main方法,從磁盤讀入這個ddl.sql文件,挨個字符讀取并判斷其編碼字節數,最終找到這樣一串字符(因為直接粘貼過來就變樣了,所以截圖)
dd.png

這里的第四個字符編碼為4位的(我本來還以為是ddl.sql文件里有四個字節的utf-8編碼才能表示的中文漢子呢~~~~~~~~~),原來是因為建表語句的comment里有亂碼,而這些亂碼里有4字節utf-8字符。

  • 為什么從公司內部的db導出的建表與存在這樣奇怪的亂碼。(還沒搞清楚。。。。)

解決方式

  • 修改數據庫字符集為utf8mb4,在java連接mysql時加上set names utf8mb4;,但是公司的dba很拽的,db字符集不支持自助修改,dba整天整天的沒反應。。。。。蛋疼
  • 在java端過濾掉這些4字節字符(這種亂碼本來就沒啥用,過濾掉豈不更好。。。~~)

正則表達式之unicode匹配

都知道正則表達式可以用[a-zA-Z]這樣的方式匹配字母,那我們的中文字符也想這樣匹配怎么辦呢,用unicode匹配吧,世界上任何一個字符都可以用unicode來表示。

  • \u開頭表示直接匹配unicode編碼
2E80~33FFh:中日韓符號區。收容康熙字典部首、中日韓輔助部首、注音符號、日本假名、韓文音符,中日韓的符號、標點、帶圈或帶括符文數字、月份,以及日本的假名組合、單位、年號、月份、日期、時間等。  
  
3400~4DFFh:中日韓認同表意文字擴充A區,總計收容6,582個中日韓漢字。  
  
4E00~9FFFh:中日韓認同表意文字區,總計收容20,902個中日韓漢字。  
  
A000~A4FFh:彝族文字區,收容中國南方彝族文字和字根。  
  
AC00~D7FFh:韓文拼音組合字區,收容以韓文音符拼成的文字。  
  
F900~FAFFh:中日韓兼容表意文字區,總計收容302個中日韓漢字。  
  
FB00~FFFDh:文字表現形式區,收容組合拉丁文字、希伯來文、阿拉伯文、中日韓直式標點、小符號、半角符號、全角符號等。  
  • -p表示匹配unicode編碼的屬性(unicode屬性有很多,類似中文標點,漢字什么什么的)

\p{xx}表示一個有屬性 xx 的字符,可以在左花括號 { 后面增加 ^ 表示取反。比如: \p{^Lu} 就等同于 \P{Lu}。

  • -P表示匹配沒有unicode編碼的屬性
    \P{xx}表示一個沒有屬性 xx 的字符

最后,用unicode正則表達式替換掉所有4個字節的utf-8編碼字符

前面講過utf-8的3個字節以內能表示的字符的unicode編碼范圍,所以直接過濾掉不在這個范圍的字符

public static final String filterCodeLargerThan3Byte(String s) {

        if (s == null) {
            return s;
        }

        return s.replaceAll("[^\\u0000-\\u007F\\u0080-\\u07FF\\u0800-\\uFFFF]", "");
    }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容