原創:打碼日記(微信公眾號ID:codelogs),歡迎分享,轉載請保留出處。
簡介
現代編程語言都抽象出了String字符串這個概念,注意它是一個高級抽象,但是計算機中實際表示信息時,都是用的字節,所以就需要一種機制,讓字符串與字節之間可以相互轉換,這種轉換機制就是字符編碼,如GBK,UTF-8
所以可以這樣理解字符串與字符編碼的關係:
- 字符串是一種抽象,比如java中的String類,它在概念上是編碼無關的,裏面包含一串字符,你不需要關心它在內存中是用什麼編碼實現的,儘管字符串在內存中存儲也是需要使用編碼機制的。
- 字節串才需要關心編碼,當我們要將字符串保存到文件中或發送到網絡上時,都需要使用字符編碼機制,將字符串轉換為字節串,因為計算機底層只認字節。
常見字符編碼方案
ASCII
全稱為American Standard Code for Information Interchange,美國信息交換標準代碼,用來編碼英文字符,一個字符佔一個字節,只用了字節中的低7位,最高位始終為0,因此只能表示2^7=128個字符。
ISO8859-1
對ASCII的擴充,添加了西歐語言、希臘語、泰語、阿拉伯語、希伯來語對應的文字符號,也稱latin-1,將ASCII中最高一位也利用起來了,能表示2^8=256個字符,當最高位是0時,編碼方式就是ASCII,所以ISO8859-1是兼容ASCII碼的。
GBK
全稱為Chinese Internal Code Specification,於1995年制定,用來編碼漢字的一種方案,一個漢字編碼為兩個字節,兼容ASCII碼編碼方案,ASCII中的英文字符編碼為一個字節。
Unicode
Unicode 的全稱是 universal character encoding,中文一般翻譯為"統一碼、萬國碼、單一碼",用於定義世界上所有的字符,避免了各個國家設計的本地字符集互相不兼容的問題。早期由於另一個組織也定義了一種與Unicode類似的方案ucs,而後與Unicode合併,故有時Unicode也稱為ucs。
注意,Unicode是一種字符集,而不是一種具體的字符編碼,要理解Unicode具體是什麼,首先要理解字符集與字符編碼的關係,一般來説,字符集定義字符與代碼點(codepoint)之間的對應關係,而字符編碼定義代碼點(codepoint)與字節之間的對應關係。
比如ASCII字符集規定A用65表示,至於65在計算機中用什麼字節表示,字符集並不關心,而ASCII字符編碼定義65應該用一個字節表示,對應為01000001,十六進制表示法為0x41,它是ASCII字符集的一種實現,也是唯一的實現。
但Unicode做為一種字符集,它沒有規定Unicode中的字符該如何編碼為字節,而UTF-16、UTF-32、UTF-8就都是Unicode的字符編碼實現方案,它們具體定義瞭如何將Unicode字符轉換為相應的字節。
UTF-32
UTF-32編碼,也稱UCS-4,是Unicode 最直接的編碼方式,用 4 個字節來表示 Unicode 字符中的 code point ,比如字母A對應的4個字節為0x00000041。它也是 UTF-*編碼家族中唯一的一種定長編碼(fixed-length encoding),定長編碼的好處是能快速定位第N個字符,便於指針運算。但用四個字節來表示一個字符,對於英文字母來説,空間佔用就太大了。
UTF-16
UTF-16編碼,也稱UCS-2,最少可以採用 2 個字節表示 code point,比如字母A對應的2個字節為0x0041。需要注意的是,UTF-16 是一種變長編碼(variable-length encoding),只不過對於 65535 之內的 code point,只需要使用 2 個字節表示而已。但是,很多歷史代碼庫在實現 UTF-16 編碼時,直接使用2字節存儲,這導致在處理超出 65535 之外的 code point 字符時,會出現一些問題,另外,UTF-16對於純英文存儲,也會浪費1倍存儲空間。
字節序與BOM
不同的計算機存儲字節的順序是不一樣的,比如U+4E2D在 UTF-16 可以保存為4E 2D,也可以保存成2D 4E,這取決於計算機是大端模式還是小端模式,UTF-32也類似。為了解決這個問題,UTF-32與UTF-16都引入了BOM機制,在文件的起始位置放置一個特殊字符BOM(U+FEFF),如果 UTF-16 編碼的文件以FF FE開始,那麼就意味着其字節序為小端模式,如果以FE FF開始,那麼就是大端模式。所以UTF-16根據大小端可區分為兩種,UTF-16BE(大端)與UTF-16LE(小端),UTF-32同理。
Unicode表示法
我們經常會看到形如 U+XXXX 或 \uXXXX 形式的東西,它是一種表示Unicode字符的方式,俗稱Unicode表示法,其中XXXX是 code point 的十六進制表示,比如 U+0041 或 \u0041 表示Unicode中的字母A。咋一看,這玩意有點類似 UTF-16 ,但要注意它是一種用英文字符串指代一個Unicode字符的方式,不是一種字符編碼,字符編碼是用字節串指代一個Unicode字符。
UTF-8
由於UTF-16用兩個字節編碼英文字符,對於純英文存儲,對空間是一種極大的浪費,所以unix之父Ken Thompson又發明了一種Unicode字符編碼——UTF-8,它對於ASCII範圍內的字符,編碼方式與ASCII完全一致,其它字符則採用2字節、3字節甚至4字節的方式存儲,所以UTF-8是一種變長編碼。對於常見的中文字符,UTF-8使用3字節存儲。
包含關係圖
亂碼又是怎麼回事?
亂碼本質上是編碼端程序與解碼端程序用的字符編碼不同導致的,比如一個程序(編碼端)使用UTF-8存儲字符串到文件中,另一個程序(解碼端)讀取時卻用GBK解碼,就會出現亂碼了。
實踐-java
String.getBytes()與new String(bytes)
String str = "好";
//字符串轉字節,使用UTF-8
byte[] bytes = str.getBytes("UTF-8");
//'好'在UTF-8下編碼為3字節e5a5bd
System.out.println(Hex.encodeHexString(bytes));
//字節轉字符串,使用UTF-8
System.out.println(new String(bytes, "UTF-8"));
//字符串轉字節,不傳字符編碼,默認使用操作系統的編碼,我開發機是Windows,默認編碼為GBK
bytes = str.getBytes();
//'好'在GBK下編碼為2字節bac3
System.out.println(Hex.encodeHexString(bytes));
//字節轉字符串,同樣使用我當前操作系統默認編碼GBK
System.out.println(new String(bytes));
對於java的String.getBytes()與new String(bytes)方法,是用來進行字符串與字節轉換的,但建議最好使用帶charset版本的方法,如String.getBytes("UTF-8")與new String(bytes,"UTF-8"),因為沒有指定字符編碼的方法,會默認使用操作系統上設置的編碼,而Windows上默認編碼經常是GBK,這就導致使用linux或mac開發的程序,運行得好好的,在Windows上卻亂碼了。
另外,像如下的InputStreamReader與OutputStreamWriter,也有帶charset與不帶charset版本的,最好也使用帶charset版本的方法。
//InputStreamReader與OutputStreamWriter也一樣,如果不指定字符編碼,就使用操作系統的
InputStreamReader isr = new InputStreamReader(in, "UTF-8");
OutputStreamWriter osw = new OutputStreamWriter(out, "UTF-8");
另外,在啓動java項目時,最好帶上jvm參數-Dfile. encoding=utf-8,這樣可以設置jvm默認編碼為UTF-8,避免程序繼承操作系統編碼,也可以像下面這樣,在項目啓動的第一行,手動設置編碼為UTF-8,這樣沒有設置jvm參數的同學也不會出現亂碼了。
//設置當前jvm默認字符編碼為UTF-8,避免繼承操作系統編碼
System.setProperty("file.encoding", "UTF-8");
實踐-linux
od與xxd
od與xxd是查看字節數據的工具,可以以十六進制、八進制、二進制、十進制的方式查看字節,非常方便,如下:
#查看'好'的十六進制,如下'好'輸出3個字節,可見echo使用了utf-8編碼
$ echo -n 好|xxd
00000000: e5a5 bd
# -b選項表示輸出01二進制形式
$ echo -n 好|xxd -b
00000000: 11100101 10100101 10111101
# od同樣可以輸出十六進制
$ echo -n 好|od -t x1
0000000 e5 a5 bd
# linux下查看ASCII碼錶
$ man ASCII
$ printf "%0.2X" {0..127}| xxd -r -ps | od -t x1d1c
iconv
iconv是用來轉換字符編碼的好工具,如下:
# iconv將echo輸出的utf-8字節轉換為gbk字節,可見中文的gbk編碼為2字節
$ echo -n 好|iconv -f utf-8 -t gbk |xxd
00000000: bac3
# 轉換為utf-16be,可見中文的utf-16編碼一般是2字節
$ echo -n 好|iconv -f utf-8 -t utf-16be |xxd
00000000: 597d
# 轉換為utf-32be,可見中文的utf-32編碼是4字節,且一般前2個字節都是0
$ echo -n 好|iconv -f utf-8 -t utf-32be |xxd
00000000: 0000 597d
其它有用工具
# Unicode表示法轉字符串
$ echo -e '\u597d'
好
# 字符串轉Unicode表示法
$ echo -n '好' | iconv -f utf-8 -t ucs-2be | od -A n -t x2 --endian=big | sed 's/\x20/\\u/g'
\u597d
# 猜測文件編碼
$ enca -L zh_CN -g -i file.txt
UTF-8
# 轉換文件編碼為UTF-8
$ enca -L zh_CN -c -x UTF-8 file.txt
mysql中的utf8mb4又是啥?
UTF-8作為Unicode的一種字符編碼方案,本來是可以編碼Unicode中的所有字符的,但早期mysql在實現utf-8時,實現時自行限制utf-8最多使用3個字節,也稱utf8mb3,導致如今普遍出現的emoji表情無法存儲,因為emoji表情要使用4個字節才能編碼,這就導致mysql又推出了utf8mb4來彌補這個缺陷。
總結
徹底理解字符編碼並不容易,主要是這個在計算機書籍上從來沒有重點介紹過,而在自己剛開始工作時,經常遇到各種亂碼問題,然後網上一通搜索胡亂設置來解決問題,但卻一直沒搞清楚為啥,直到自己摸熟iconv這個命令後,才真正理解清楚。
往期內容
真正理解可重複讀事務隔離級別
Linux文本命令技巧(下)
Linux文本命令技巧(上)
原來awk真是神器啊
常用網絡命令總結