閱讀158 返回首頁    go 阿裏雲 go 技術社區[雲棲]


扯談下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

https://www.unicode.org

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

  上一篇:go Oracle中的substr方法
  下一篇:go Makefile生成工具和方法(autoconf 和 automake)