效果圖html
字符java
字符是早於計算機而存在,從人類有文明那時起,人們就用一個個符號表明世間萬象。如ABC,如「1、2、三」。正則表達式
字符集算法
字符集是全部字符的集合。工具
XXX字符集網站
給字符集中的每個字符套上一個序號後的字符集。常見的XXX字符集有ASCLL字符集、Unicode字符集等等,不一樣種字符集爲每一個字符編的序號不一樣,包含的字符數量也不一樣。編碼
GBK、UTF-8spa
GBK、UTF-8是一種編碼編碼格式。固然,你也能夠說unicode是一種編碼格式,由於它的的確確爲每一個字符編了一個碼,沒錯,但是unicode的編碼徹底沒有規律,最多隻能把其當映射表用。.net
咱們知道,計算機只能識別1和0,假如計算機存儲中文字符「字」在硬盤,確定是存儲一串二進制串。3d
那麼問題來了,中文字符【字】在unicode字符集中的序號是23383,那麼直接把23383轉化成2進製爲101101101010111,而後存儲在計算機裏面,等須要的時候把101101101010111串拿出來,轉成23383,再根據unicode映射表,找到中文字符【字】不就好嗎?
答案是否認的,若是是這樣的話,那計算機怎麼知道多少個一、0才表明一個字符呢?因此咱們須要一種編碼格式,把23383編碼成有規律的一、0串,以便計算機讀取。
而GBK和UTF-8即是兩種不一樣的有規則的編碼格式。
例如:以UTF-8爲例子,假如咱們所在的環境使用的是unicode字符集,那麼「字」在unicode字符集中的序號是23383,轉成二進制是101101101010111,使用UTF-8爲其編碼,以一種特定的算法(下面會具體講這種算法),把101101101010111轉化成11100101 10101101 10010111三個字節的二進制串,再存儲到硬盤中,計算機在讀取的時候,假如咱們指定了讓計算機以UTF-8編碼格式讀取並解碼,計算機就會把這三個字節拿出來,倒着轉回去,就能獲得【字】這個中文字符了。
亂碼的根源:
假如咱們存儲的時候,使用GBK編碼格式編碼,存儲到硬盤,而從硬盤讀取出來後,在「倒着轉回去」這個步驟卻使用UTF-8編碼格式轉回去,算法不一樣,那麼就可能出現亂碼。
如何避免亂碼:
以什麼編碼格式存儲,就用什麼編碼格式解。
可是,假如用戶A使用GBK編碼對「字」進行編碼,而用戶B並不知情,也沒A的聯繫方式,跟A約定不了,沒法得知硬盤中的數據是以什麼編碼格式編碼的,怎麼辦呢?
解決亂碼的思路:
一、隨意使用一種編碼格式解碼,看解碼後的字符串是否亂碼,若是是亂碼,就用另外一種編碼格式解碼。但該方法可能誤判。
二、UTF-8編碼格式有必定的規律,咱們能夠經過正則表達式來驗證是不是通過UTF-8編碼後的。
JAVA自帶檢測亂碼
1 boolean b = java.nio.charset.Charset.forName("GBK").newEncoder().canEncode(str);
當開始接觸這種方法時,原覺得java能幫咱們判斷亂碼,就能夠高枕無憂了,後來發現,該方法的成功率並不高。
但咱們能夠先用此方法作第一步檢測,若是判斷不出來,再使用第2種方法。
UTF-8的編碼規律
UTF-8形式的二進制,當一個字節時,兩個字節時,3、4、5、六個字節時,都有必定的格式:
1字節 | 0xxxxxxx |
2字節 | 110xxxxx 10xxxxxx |
3字節 | 1110xxxx 10xxxxxx 10xxxxxx |
4字節 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
5字節 | 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
6字節 | 111111x0 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
很明顯,字節數不同的話,第一個字節是不一樣的,因此第一個字節可用用來表示該字符究竟佔用了多少個字節。
當計算機讀取到以0xxxxxxx開頭的字節,那麼就表明這個字節獨自就已經表示某個字符了,計算機將把這個字節單獨拿出來解碼。
當計算機讀取到以110xxxxx開頭的字節,那麼就表明兩個字節才能表示某個字符,計算機就把這個字節以及它後面的一個字節拿出來,表明一個字符進行解碼。
……
而除了第一個字節外,後面的字節都是統一的10xxxxxx格式。
有了上面的有規則的格式,按到理咱們就可使用正則表達式來檢測一個二進制串是不是UTF-8編碼後的串,但代碼中操做二進制並不方便,結合URL爲16進制的特色,咱們能夠用正則表達式判斷16進制的串。
如何構造正則表達式
咱們先看看這種編碼格式前一個字節的範圍:
二進制 | 十六進制 | |
1字節 | 00000000~01111111 | 00~7f |
2字節 | 11000000~11011111 | c0~df |
3字節 | 11100000~11101111 | e0~df |
4字節 | 11110000~11110111 | f0~f7 |
5字節 | 11111000~11111011 | f8~fb |
6字節 | 11111100~11111101 | fc~fd |
以上的範圍可用計算機自行驗證:
後面格式相同的字節10xxxxxx的範圍:
10000000~10111111 | 80~bf |
按照這種格式,UTF-8編碼格式最多可用用來表示一個1+5*6=31位的二進制串,共使用6個字節。
按照這種規律,咱們先練一下手,嘗試把「字」轉化爲UTF-8的十六進制:
java使用的字符集是unicode的,因此咱們以unicode爲例子。
一、找出「字」在unicdoe字符集中的序號:
public static void main(String[] args) { System.out.println((int)'字'); }
結果爲:23383
2、把23383轉化二進制:
23383 | 101101101010111 |
可用看出,二進制共15位,按照UTF-8的編碼格式,得用3個字節來表示。
咱們把101101101010111從後往前分紅三組:101,101101,010111
填充到3字節的UTF-8編碼格式中爲:
1110xxxx 10xxxxxx 10xxxxxx
11100101 10101101 10010111
3、使用計算器把二進制轉化爲16進製爲:
OxE5 OxAD Ox97
4、使用網上的工具驗證一下,結果吻合,說明這種規律是正確的。
上面已經介紹了UTF-8的規律,那麼咱們藉助強大的正則表達式,就能夠判斷一個URL串是通過什麼編碼格式編碼的了。
先把上面的表複製下來容易觀察:
二進制 | 十六進制 | |
1字節 | 00000000~01111111 | 00~7f |
2字節 | 11000000~11011111 | c0~df |
3字節 | 11100000~11101111 | e0~df |
4字節 | 11110000~11110111 | f0~f7 |
5字節 | 11111000~11111011 | f8~fb |
6字節 | 11111100~11111101 | fc~fd |
1字節時:[\\x00-\\x7f]---------------------------------1
2字節時:[\\xc0-\\xdf][\\x80-\\xbf]-------------------2
3字節時:[\\xe0-\\xef][\\x80-\\xbf]{2}--------------3
4字節時:[\\xf0-\\xf7][\\x80-\\xbf]{3}--------------4
5字節時:[\\xf8-\\xfb][\\x80-\\xbf]{4}--------------5
6字節時:[\\xfc-\\xfd][\\x80-\\xbf]{5}--------------6
使用或組合在一塊兒就是:^([\\x00-\\x7f]|[\\xc0-\\xdf][\\x80-\\xbf]|[\\xe0-\\xef][\\x80-\\xbf]{2}|[\\xf0-\\xf7][\\x80-\\xbf]{3}|[\\xf8-\\xfb][\\x80-\\xbf]{4}|[\\xfc-\\xfd][\\x80-\\xbf]{5})+$
判斷過程是這樣子的:例如【字】通過UTF-8編碼後,爲:%e5 %ad %97,共3個字節,符合第3字節的狀況,第一個字節e5在[\\xe0-\\xef]範圍內,後兩個字節ad和97都在[\\x80-\\xbf]範圍內。
因此咱們能夠說這個字符是通過UTF-8編碼的。咱們就可使用UTF-8編碼格式對其進行解碼了。
java代碼以下:
1 protected static final Pattern utf8Pattern = Pattern.compile("^([\\x00-\\x7f]|[\\xc0-\\xdf][\\x80-\\xbf]|[\\xe0-\\xef][\\x80-\\xbf]{2}|[\\xf0-\\xf7][\\x80-\\xbf]{3}|[\\xf8-\\xfb][\\x80-\\xbf]{4}|[\\xfc-\\xfd][\\x80-\\xbf]{5})+$"); 2 Matcher matcher = utf8Pattern.matcher(pureValue); 3 if (matcher.matches()) { 4 return "UTF-8"; 5 } else { 6 return "GBK"; 7 }
缺陷
使用上面的方法,貌似沒什麼問題,不過GBK編碼後是以兩個兩個字節呈現的,而UTF-8也有兩個字節的狀況,因此當一個字符經GBK編碼後,轉化爲16進制,而恰好這個16進制的範圍落入UTF-8的兩個字節的範圍,那麼就會被誤判成UTF-8,從而致使解碼錯誤。那真的有可能會出現這種狀況嗎?
答案是會的,咱們查看下GBK簡體中文編碼表。
發現有一部分範圍落入了UTF-8的二進制範圍了。
從:
一直到:
即UTF-8十六進制中兩個字節的範圍[\\xc0-\\xdf][\\x80-\\xbf],GBK都有。
例如上面表的第二個中文【愧】,愧的GBK十六進制是C0 A0,那麼徹底符合UTF-8正則表達式中二字節的[\\xc0-\\xdf][\\x80-\\xbf]這個判斷,因此會被誤認爲是UTF-8編碼。
注:該缺陷第一次看,是在下方「參考"的第一篇博客裏,嘗試了一下,的確有缺陷。
嘗試修復缺陷
根據下面"參考"的第一篇博客,修復的思路是把重複的區域都認爲是GBK編碼。
咱們截取正則表達式的前兩種狀況(一字節、二字節的狀況)來排除:^([\\x01-\\x7f]|[\\xc0-\\xdf][\\x80-\\xbf])+$
假如某個16進制串match該正則表達式,就認爲是GBK編碼的。
修改後的代碼爲:
1 protected static final Pattern utf8Pattern = Pattern.compile("^([\\x01-\\x7f]|[\\xc0-\\xdf][\\x80-\\xbf]|[\\xe0-\\xef][\\x80-\\xbf]{2}|[\\xf0-\\xf7][\\x80-\\xbf]{3}|[\\xf8-\\xfb][\\x80-\\xbf]{4}|[\\xfc-\\xfd][\\x80-\\xbf]{5})+$"); 2 protected static final Pattern publicPattern = Pattern.compile("^([\\x01-\\x7f]|[\\xc0-\\xdf][\\x80-\\xbf])+$"); 3 Matcher publicMatcher = publicPattern.matcher(str); 4 if(publicMatcher.matches()) { 5 return "GBK"; 6 } 7 8 Matcher matcher = utf8Pattern.matcher(str); 9 if (matcher.matches()) { 10 return "UTF-8"; 11 } else { 12 return "GBK"; 13 }
又一缺陷
但這樣一來,本來是一個字節或兩字節,且是UTF-8編碼的,就會被誤判爲GBK。。。
可是,這總比被誤判成UTF-8好,由於咱們查看Unicode編碼表:
能夠發現,第一個中文是「一」,轉化爲UTF-8的話已經排到3個字節去了,因此2個字節內不會出現中文。
可是GBK中,中文是兩個字節的。
因此,採用上面的修復缺陷的方法,能夠保證中文不會亂碼。對於某些網站,只需保證中文不會亂碼便可,好比說國內的各類中文購物網站。這些網站中商品的標題通常都是中文的,用戶通常以中文搜索,咱們儘量保證中文不亂碼便可。
因此,該技術仍是有必定用處的。
參考
一、http://www.cnblogs.com/chengmo/archive/2011/02/19/1958657.html
二、http://www.cnblogs.com/chengmo/archive/2010/10/30/1864004.html
四、GBK簡體中文表
若有錯誤的地方,歡迎指出。