2015年7月14日 星期二

Python, JavaScript, C++, Java 和 Unicode 以及 UTF-8 等編碼

基本觀念

Unicode 是一張表,定義每個文字對應的數字是什麼,比方說 'A' 是 65, '我'是 25105。

UTF-8, UTF-16, UTF-32, UCS-2, UCS-4 等則是 Unicode 的各種編碼。所謂的編碼,是某種定好的儲存格式 (或說 serialization/deserialization format 也許比較好懂?), 方便讓不同的應用程式之間傳遞資料。

比方說應用程式 A 寫入文字到檔案裡,應用程式 B 從檔案讀出文字,兩者要說好用的編碼格式 (或能從資料辨別使用的編碼格式), 才有辦法互通資料。舉例來說, 網路傳輸常用 HTTP 作為溝通協定, HTTP 有提供 client 或 server 指定傳輸內容編碼的方法 (Accept-Charset 和 Content-Type), 可以指明是用 UTF-8 或其它編碼。

寫程式的時候,基本上是假設外部讀來的資料是某種 Unicode 編碼 (最常用 UTF-8 ), 內部使用 Unicode 處理,要傳出去 (寫入硬碟/網路傳輸) 前再轉回 Unicode 編碼。用 Unicode 的好處是,可以正確計算字串長度, 這影響到使用 length, index, 取出 substring, 比對 substring (包含 regexp) 等字串 API。

這裡有以前寫的介紹,有多提一點相關的事。

Python

Python 2 就有定義 type unicode 和 type str。type str 可看成一般性的 binary, 以 byte 為單位。Python 2 的字串 '...' 的型別是 str, 要特別用 u'...' 才會改用 unicode。

有了 type unicode, 處理 unicode 比較輕鬆:

  1. 外部讀進來的字 (UTF-8) 存在 type str, 馬上轉成 unicode
  2. 函式的輸入輸出都用 unicode (len(), [] 的行為正確)
  3. 要輸出到螢幕、檔案、網路時再轉成 UTF-8。

參考 All About Python and UnicodeUnicode in Python: Common Pitfalls 了解細節。

另外, Python 可能用 UCS-2 或 UCS-4 實作 Unicode, 可從 sys.maxunicode 的值得知, 65535 表示用 UCS-2。因為 UCS-2 / UCS-4 每個字分別占 2 / 4 bytes, 對使用的記憶體的量有很大的影響,不同作業系統會有不同考量, 像 Mac OS X 用 UCS-2, Ubuntu 12.04 用 UCS-4。然後在 Python 3.3 後都支援 UCS-4 了 (因為有實作 PEP 0393 -- Flexible String Representation)。

例如 u'𝌆' 會被當成兩個字 [*1]:

In [1]: u'我'
Out[1]: u'\u6211'

In [2]: len(u'我')
Out[2]: 1

In [3]: u'𝌆'
Out[3]: u'\U0001d306'

In [4]: len(u'𝌆')
Out[4]: 2

Btw, Python 3 改變字串預設的型別: '...' 是 unicode, 要用 b'...' 才會用 str,用意是希望大家預設就用 Unicode 避免混亂,不過卻造成 2 升 3 的一些問題。直接引用以前碎碎唸的心得:

在 python2.7 踩到 python3 unicode 的雷,import unicode_literals 會讓 'abc' 自動變成 u'abc' ( 原本的行為是 ‘abc' == b'abc') ,難怪怎麼看都不太對勁,third-party code 寫 'abc',別的 third-party package 預期收到 raw string 卻收到 unicode string, 於是回報 TypeError: 'unicode' does not have the buffer interface

用 ipdb 除錯時覺得相當奇怪,一樣的程式碼 (如 s = 'abc'),在 ipdb 內執行得到的結果是 raw string, 但原本的程式執行時卻變 unicode ....

搜了一下才看到也有不少人反應這點: Proposal to not use unicode_literals #22 https://github.com/PythonCharmers/python-future/issues/22

JavaScript

JavaScript’s internal character encoding: UCS-2 or UTF-16?: 詳細到不行的介紹,沒耐性就直接看文中的 conclusion 吧。

結論是 JavaScript 直接用 UCS-2 表示字元, 不像 Python 分成 str 和 unicode。開發者幾乎不用操心 Unicode、UTF-8 之類的事, 反正在 JavaScript 內都是用 UCS-2 表示。缺點是超出 65535 的 Unicode 不會被正確處理:

> '我'.length
1
> '𝌆'.length
2

不過網頁還是可以正確顯示字的外觀,大概是因為瀏覽器內部用 UTF-16 處理,字的 Unicode 仍然是對的,畫字時有選對 font glyph。但是受限於 JavaScript 的語言規範,只能斷成多個字表示。

C++

C++ 沒有定義型別 Unicode, 要自己用 std::string, std::wstring 或其它型別表示。

查到幾種轉換方法

  • 用 C++11
  • 用 Boost
  • 用 icu
  • utf8-cpp

最後一個作法最簡單, include 幾個 header 檔即可,不像其它方法要改 compiler 參數或引入一大包函式庫。所以我後來是用 utf8-cpp, 待要非得用到其它方法時再來研究為啥需要其它方法。stackoverflow 裡有很多討論 (由此可見 C++ 使用 Unicde 的方法有多混亂....), 這是其中一篇

Java

Java 的基本單位 char 一樣用 UCS-2 實作, 但 String 提供另外的 API 可正確存取 Unicode。直接引用別人的結論:

Han Guokai wrote on 20th January 2012 at 18:28:

In Java5, ‘char’ means code unit (16-bit), not character (code point). One character uses one or two chars. String’s length method return the char count. And there are some additional methods based on code points, i.e. codePointCount. See: http://docs.oracle.com/javase/1.5.0/docs/api/java/lang/String.html

比方說 Java String 提供 charAtcodePointAt。後者是正確的 Unicode 字元,而前者是 char, 遇到 UCS-2 外的字一樣會出錯。

結論

總算弄清楚過去常用的四種語言實作 Unicode 和 Unicode 編碼的方法,結果四種語言有四套作法, 真是出乎意料出外...。推薦閱讀JavaScript’s internal character encoding: UCS-2 or UTF-16? 了解 BMP, supplementary planes 和 surrogate pairs, 會比較清楚 JavaScript 和 Java 使用 UCS-2 作為字元定義遇到的問題 (為了省記憶體, 是必要的 trade-off)。

備註

1. Blogspots 無法正式顯示這個字, 可從這裡看到測試用的字。https://codepoints.net/ 可找到更多範例。

2015/07/19 更新

Scott 指正, 更新內文對 Python Unicode 實作的描述。

1 則留言:

  1. 不知道你需要什麼樣的功能

    http://www.cppstdlib.com/code/cppstdlib-code.tgz

    // convert UTF-8 string to wstring
    std::wstring utf8_to_wstring (const std::string& str)
    {
    std::wstring_convert> myconv;
    return myconv.from_bytes(str);
    }

    這是 c++11 提供的標準程式庫, 可以處理 utf8 轉 ucs-4, 需要用 g++-5

    回覆刪除

在 Fedora 下裝 id-utils

Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件 ,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball ,解開來編譯 (./configure && make)就是了。 但編譯後會遇到錯誤: ./stdio.h:10...