扯談下UTF-8
前言:
本來想翻譯這篇文章的(作者是utf-8編碼,golang發明者之一):
UTF-8: Bits, Bytes, and Benefits,https://research.swtch.com/utf8
一則翻譯起來很痛苦,二則覺得這篇文章有些地方可能說得不是太明白,所以結合其它的一些東東扯談下utf-8。
Unicode:
先扯談下Unicode。
Unicode就是為每一個字符(各種語言的各種字符)分配一個數字。所以它實際上是一個表,記錄了字符和數字的對應關係。
比如漢字“你”,對應的數字是20320,16進製是4F60。
目前Unicode的範圍從 U+0000 到 U+10FFFF 。有UTF-8,UTF-16,UTF-32三種編碼方式。
其中UTF-8對應1到4個8-bit,UTF-16對應1到2個16-bit,UTF-32對應1個32-bit。
下麵這個表,很清晰地總結了各種編碼方式(from:
https://www.unicode.org/faq/utf_bom.html ):
Name | UTF-8 | UTF-16 | UTF-16BE | UTF-16LE | UTF-32 | UTF-32BE | UTF-32LE |
---|---|---|---|---|---|---|---|
Smallest code point | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 |
Largest code point | 10FFFF | 10FFFF | 10FFFF | 10FFFF | 10FFFF | 10FFFF | 10FFFF |
Code unit size | 8 bits | 16 bits | 16 bits | 16 bits | 32 bits | 32 bits | 32 bits |
Byte order | N/A | <BOM> | big-endian | little-endian | <BOM> | big-endian | little-endian |
Fewest bytes per character | 1 | 2 | 2 | 2 | 4 | 4 | 4 |
Most bytes per character | 4 | 4 | 4 | 4 | 4 | 4 | 4 |
曆史的悲劇:
因為曆史原因,曾經人們以為用兩個8-bit,可以表示任意一字符,最初的Unicode標準就是16-bit的。所以Java中的char類型,C++中的wchar_t(gcc當作32-bit),QT中的QString,windows的底層Unicode的支持,都是16-bit的,所以造成了很多悲劇。
wchar_t實際上是個過時的東東,所以在C++11中增加了char16_t和char32_t類型,不過因為各家編譯器的實現,標準庫的實現,及語法等,實際使用還是相當相當的蛋疼。
這些悲劇一是unicode標準本身很比較晚才成熟,二則C/C++一直沒有把unicode支持標準化(所以QT自己搞了一套,微軟自己也搞了套)。
不過話說回來,這些悲劇不能全怪C++,隻能說Unicode標準本身就蛋疼。據我所認識的編程語言中,隻有後來比較晚出現的語言才比較好地支持unicode,比如golang,python3。
像JavaScript,根本沒有Unicode這概念。
https://www.ruanyifeng.com/blog/2014/12/unicode.html Unicode與JavaScript詳解
UTF-8:
上麵扯遠了,再回來說下UTF-8。
UTF-8的編碼規則:
UTF-8的編碼規則可以看阮一鋒寫的文章:https://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
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 下麵,還是以漢字“嚴”為例,演示如何實現UTF-8編碼。 已知“嚴”的unicode是4E25(100111000100101),根據上表,可以發現4E25處在第三行的範圍內(0000 0800-0000 FFFF),因此“嚴”的UTF-8編碼需要三個字節,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然後,從“嚴”的最後一個二進製位開始,依次從後向前填入格式中的x,多出的位補0。這樣就得到了,“嚴”的UTF-8編碼是“11100100 10111000 10100101”,轉換成十六進製就是E4B8A5。
要注意的是在UTF-8編碼中,非ASCII字符的編碼字串中不會出現0X00,即'\0'。這個很重要,是UTF-8有很多特性的重要原因。
UTF-8編碼的優點:
UTF-8的編碼規則,讓它有很多特性:
-
兼容ASCII。ASCII編碼的文件即同時也是UTF-8編碼的文件。
-
可以從二進製數據流中識別出ASCII字符,比如有一個字節是0x7A,那麼它肯定是字符'z',因為它的最高為是0(參見上麵的UTF-8的編碼規則,所有的非ASCII字符的UTF-8編碼的字節的最高位都是1)。
-
子串搜索可以直接按字節進行搜索,可以使用C語言原有的函數,如strchr,strstr(原因參考上麵的UTF-8的編碼規則)。
-
大多數處理8-bit編碼文件的程序可以安全地處理utf-8文件(原因參考上一點)。
-
utf-8編碼的順序和Unicode中碼位(code point)的順序是一致的,所以像unix工具,join,ls,sort等不用顯式地指明是utf-8編碼。
-
utf-8編碼是沒有字節順序的,UTF-8編碼的文件可以不用寫BOM頭。像UTF-16或者UTF-32編碼的文件就要寫入BOM頭,否則隻能猜測了。(其實,文本編輯器都是讀入一段數據,再猜測到底是什麼編碼。mozilla有個開源程序jChardet,可以猜什麼編碼,但是實測並不是很準確。對於沒有指定編碼的網頁,瀏覽器隻好去猜測所使用的編碼,像chrome瀏覽器有時就會猜錯,貌似錯誤率比IE要高。)
utf-8編碼的缺點:
1.utf-8編碼是好,但是因為它是變長的!所以用一個什麼類型來表示一個utf-8字符?
這個在C++中還是無解,其實在其它的編程語言中同樣是無解。至於go語言,它采取了一個折中的辦法,在曆遍字符串時,得到的是一個rune類型(實際即int32)。
另外,假定有一個大文件,你修改了一個字符,那麼有可能整一個文件都有重新保存。。
UTF-16編碼盡管也有可能要重新保存整個文件,但是概率比較小,因為大部分常用的字符都可以用一個16-bit來表示。
2.不能實現O(1)的隨機訪問
所以像文本編輯器等要內部再用一個數組來記錄每一個字符的位置。
不過,像隨機訪問,這樣的操作是很少的。
對於編程語言來說,問題不大,因為大部分編程語言中string都是不要修改的(貌似隻有C++中string是可以修改的)。
所以通常隻有曆遍操作,而曆遍操作,對於UTF-8,UTF-16,UTF-32都是O(n)的時間複雜度(當然,如果較真,UTF-32編碼要快一些)。
其實上麵的缺點,同樣是UTF-16編碼的缺點(它也是變長的1個16-bit或者2個16-bit),UTF-16編碼還有一個重要的缺點是要指明字節序。
UTF-32編碼的缺點是要指明字節序和太浪費空間(如果全是ASCII字符,那麼要浪費3倍的空間!),UTF-32編碼的優點是可以O(1)實現隨機訪問。
其它的一些東東:
受Windows API的影響,話說我以前是UTF-16黨(wchar_t),但是後來發現它並不是定長的(不能用像s[i]這樣的代碼來訪問一個字符),很傷心,慢慢改為用utf-8編碼了,但是utf-8編碼不能隨機訪問,的確也是個問題。
所以現在我是“無-黨-派-人-士”:) 。
國外還有人專門做了個網站來推廣utf-8編碼:https://utf8everywhere.org/
還有專門吐槽UTF-16的:https://programmers.stackexchange.com/questions/102205/should-utf-16-be-considered-harmful
Unicode在線查詢的方法:
https://en.wikipedia.org/wiki/List_of_Unicode_characters
https://www.unicode.org/charts/
4個byte的utf-8字符串的一些例子:
https://en.wikibooks.org/wiki/Unicode/Character_reference/1D000-1DFFF
這裏還有一個測試utf-8解碼是否正確的測試例子:
https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
這裏有一個Unicode編碼清單,比較實用:
https://witmax.cn/unicode-list.html
python3中的unicode:
在python3中,字符串就是Unicode字符串。在CPython的代碼中可以看到,對UTF-8,UTF-16,UTF-32都實現了支持。在創建一個字符串時(比如,解析“print('abc中國')”語句,在cmd窗口下,'abc中國'的編碼通常是cp936,即gbk編碼),如果是UTF-16或者UTF-32編碼,則直接創建一個對應的字符串對象即可。如果是其它編碼,則先轉換為UTF-8編碼,再創建一個字符串對象。
java裏如何處理大於U+FFFF(即4byte的UTF-16編碼)的字符:
java裏用char來表示一個字符,但是char卻不能表示大於U+FFFF的字符,因為char隻有兩個byte。上麵說了java出現時unicode標準還沒有成熟,所以這是一個曆史遺留問題。
那麼如何在java裏表示和處理大於U+FFFF的字符?參考這裏:
https://stackoverflow.com/questions/9834964/char-to-unicode-more-than-uffff-in-java
// This represents U+10FFFF String x = "\udbff\udfff";或者
String y = new StringBuilder().appendCodePoint(0x10ffff).toString();還提供了一些函數來處理,更多的可以直接參考String類的注釋。
System.out.println(y); System.out.println("codePoint:" + y.codePointAt(0)); System.out.println("codePoint len:" + y.codePointCount(0, y.length()));
總結:
鑒於UTF-8編碼有這麼多優點,UTF-8編碼會越來越流行。據google的統計數據,超過50%的網頁,是utf-8編碼。很多工具默認編碼都是UTF-8的,比如python3的解析器,GCC。
通常來說UTF-8編碼是優先選擇,不過,如果是一些特殊應用,要用到O(1)的隨機訪問字符串,應該使用UTF-32編碼。
參考:
UTF-8: Bits, Bytes, and Benefits
https://research.swtch.com/utf8
UTF-8, UTF-16, UTF-32 & BOM https://www.unicode.org/faq/utf_bom.html
字符編碼筆記:ASCII,Unicode和UTF-8
https://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
The Go Programming Language Specification https://golang.org/ref/spec
微軟的Unicode的一個文檔:
https://msdn.microsoft.com/en-us/goglobal/bb688113.aspx
最後更新:2017-04-02 16:48:14