效果圖
字符
字符是早於計算機而存在,從人類有文明那時起,人們就用一個個符號代表世間萬象。如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
4、GBK簡體中文表
http://www.cnblogs.com/xiaoMzjm/p/4648175.html