告別亂碼,針對GBK、UTF-8兩種編碼的智能URL解碼器的java實現(轉)


效果圖


 

 

字符


字符是早於計算機而存在,從人類有文明那時起,人們就用一個個符號代表世間萬象。如ABC,如“一、二、三”。

字符集


字符集是所有字符的集合。

 

XXX字符集


給字符集中的每一個字符套上一個序號后的字符集。常見的XXX字符集有ASCLL字符集、Unicode字符集等等,不同種字符集為每個字符編的序號不同,包含的字符數量也不同。

GBK、UTF-8


  GBK、UTF-8是一種編碼編碼格式。當然,你也可以說unicode是一種編碼格式,因為它的的確確為每個字符編了一個碼,沒錯,可是unicode的編碼完全沒有規律,最多只能把其當映射表用。

  我們知道,計算機只能識別1和0,假如計算機存儲中文字符“字”在硬盤,肯定是存儲一串二進制串。

  那么問題來了,中文字符【字】在unicode字符集中的序號是23383,那么直接把23383轉化成2進制為101101101010111,然后存儲在計算機里面,等需要的時候把101101101010111串拿出來,轉成23383,再根據unicode映射表,找到中文字符【字】不就好嗎?

  答案是否定的,如果是這樣的話,那計算機怎么知道多少個1、0才代表一個字符呢?所以我們需要一種編碼格式,把23383編碼成有規律的1、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約定不了,無法得知硬盤中的數據是以什么編碼格式編碼的,怎么辦呢?

解決亂碼的思路:

1、隨意使用一種編碼格式解碼,看解碼后的字符串是否亂碼,如果是亂碼,就用另一種編碼格式解碼。但該方法可能誤判。

2、UTF-8編碼格式有一定的規律,我們可以通過正則表達式來驗證是否是經過UTF-8編碼后的。

 

JAVA自帶檢測亂碼


 

1  boolean b = java.nio.charset.Charset.forName("GBK").newEncoder().canEncode(str);

 

當開始接觸這種方法時,原以為java能幫我們判斷亂碼,就可以高枕無憂了,后來發現,該方法的成功率並不高。

但我們可以先用此方法做第一步檢測,如果判斷不出來,再使用第2種方法。

 

UTF-8的編碼規律


UTF-8形式的二進制,當一個字節時,兩個字節時,三、四、五、六個字節時,都有一定的格式:

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為例子。

1、找出“字”在unicdoe字符集中的序號:

?
1
2
3
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中,中文是兩個字節的。

所以,采用上面的修復缺陷的方法,可以保證中文不會亂碼。對於某些網站,只需保證中文不會亂碼即可,比如說國內的各種中文購物網站。這些網站中商品的標題一般都是中文的,用戶一般以中文搜索,我們盡可能保證中文不亂碼即可。

所以,該技術還是有一定用處的。

 

參考


1、http://www.cnblogs.com/chengmo/archive/2011/02/19/1958657.html

2、http://www.cnblogs.com/chengmo/archive/2010/10/30/1864004.html

3、unicode編碼表

4、GBK簡體中文表

http://www.cnblogs.com/xiaoMzjm/p/4648175.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM