歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> Linux教程 >> 也談UTF-8編碼

也談UTF-8編碼

日期:2017/2/28 14:25:57   编辑:Linux教程

今天的早些時候,Node.js發布了一個更新,它會影響到轉化到緩沖區中的無效UTF-8字符串的處理。我又得去檢查一遍websocket-driver的中UTF-8校驗的代碼了,並且我發現自己又忘記了如何使用正則去進行校驗了。我先把它從網頁上拷貝了下來,過了一會兒才終於徹底搞明白它的工作原理了。如果你寫的程序是進行文本處理的,你很可能也需要了解這個,因此我覺得我應該把它給寫下來。

首先你需要知道的是Unicode和UTF-8並不是一回事。Unicode是一個標准,它的目標是將有限的數字分配給全世界書寫系統中的所有字符及文字。比如說,數字65,或者說U+0041,它對應的是大寫字母’A’,90也就是U+005A對應的是大寶字母 ‘Z’,而32/U+0020是空格。U+02A4是字符‘ʤ’, U+046C是 ‘Ѭ’, U+0BF5 是‘௵’, 等等。總的說來,這些數字或者說’代碼點(Code Point)’的范圍會到U+10FFFF也就是1,114,111.

一個Unicode字符串,也就是一個字符序列,實際上就是從0到1,114.111這些數字的一個序列。這些數字是如何轉化成你在屏幕上看到的字符的,這取決於你用什麼字體去渲染它了。當我們通過一個TCP連接將文本發送出去,或者保存到磁盤中的時候,我們會將它存儲成一個定長字節的序列。一個8比特的字節只能表示256個值,那我們如何去表示1,114,112個可能的代碼點呢?這就是編碼出場的時候了。

UTF-8是Unicode眾多編碼中的一種。編碼定義了字節序列和代碼點序列之間的映射關系,並告訴我們如何在它們之間進行轉換。UTF-8是WEB上常用的編碼,並被作為WebSocket協議的文本消息的編碼。

那麼UTF-8是如何工作的?首先需要知道的是我們不能將所有的代碼點都映射到單個字節上:很多代碼點的值都太大了。甚至我們都不能用它來表示00到FF,因為這樣的話,更高的值就沒法表示了。不過我們可以使用從00到7F這個范圍(0到127),留下80到FF來表示其它的代碼點。前128個代碼點就通過單個字節的低7比特位來表示:

  1. U+0000 to U+007F:
  2. 0000000000--7F01111111

這就是UTF-8的獨特之處:它並沒有使用3個字節來表示所有的代碼點(1,114,111是21比特),而是用了一個變長的字節,從1字節到4字節。前128個代碼點每個都對應著一個字節,剩下的代碼點都通過余下的128個字節的組合來表示(注:一個字節8比特有256個取值,單字節的UTF-8編碼用了低7位的128個,剩下的用於其它代碼點)。 這樣做有兩個好處,盡管有一個好處主要是針對程序員或者英語使用者的。第一個好處是UTF-8是向下兼容ASCII的:所有有效的ASCII文檔都是一個有效的UTF-8文檔,它們一一對應。第二個好處,這也是第一的結果,也就是說我們在傳輸英文文本的時候,不用使用2個或3個字節來表示。

單字節編碼的區間內有7個比特是我們可以用的。為了表示更大的值,我們需要更多的字節,UTF-8定義的雙字節由110xxxxx 10yyyyyy形式的字節對組成。x和y的比特是可變的,也就是有11個比特可以使用,加起來就到了U+07FF。

  1. U+0080 to U+07FF:
  2. 11000010 C2 -- DF 11011111
  3. 1000000080-- BF 10111111

也就是說,代碼點U+0080成了字節C2 80而代碼點U+07FF是DF BF。需要注意的是,如果使用的空間超出實際所需的話則是錯誤的:C1 BF或者說11000001 10111111會被理解成U+007F,但你可以只用一個字節就能表示這個代碼點,因此C1 BF不是一個合法的字節序列。

一般來說,多字節代碼點由一個特殊比特位的字節(大於80的字節,也就是高位為1的)後面跟著一個或多個10xxxxxx形式的字節來組成。後面的字節可用的范圍是80到BF。底於80的字節被用作單字節的代碼點,如果在多字節編碼中出現它們則是錯誤的。首字節的值會告訴我們它後面有多少個字節。

下面繼續講3字節的碼點,它們是1110xxxx 10yyyyyy 10zzzzzz的形式,我們有16個比特的數據可用,這樣我們的碼點可以到達U+FFFF。然而,現在我們碰到了一個歷史遺留問題。Unicode最早是在Unicode 88白皮書上描述的,上面是這麼說的:

將字符編碼從8位擴展到16位是非常明智的,確實如此,以至於剛想到的時候還有點震住了。 16個字節可以提供最多65536個不同的碼值,這足夠對全世界的所有字符進行編碼了嗎?由於’字符‘本身的定義也是文本編碼方案設計中的一部分,討論這個問題是沒有意義的,除非問題改成這樣:有沒有可能重新建立一種有效的字符的定義,使得全世界的字符的總數小於65536? 答案是肯定的。 – Joseph D. Becker PhD, ‘Unicode 88′

當然了,最終表明答案是否定的,你可能也猜到了現在的代碼點一共有1,114,112個。在UTF-16設計 的時候——這是一個固定雙字節的編碼規范——人們發現16個比特無法編碼所有的已知字符。因此,Unicode標准保留了一個特殊的代碼點區間以便UTF-16用來編碼大於FFFF的值。這些值會通過4個字節來進行編碼,也就是兩個標准的代碼點,前兩個字節的范圍是D8 00 到DB FF,而後兩個字節的范圍是DC 00 到DF FF。U+D800 to U+DFFF范圍內的代碼點又被稱作代理,UTF-16使用代理對(surrogate pairs)來表示更大的值。沒有字符會被分配給這些代碼點,也沒有任何編碼方式會去使用它們。

因此對於3字節的編碼,我們實際上只能編碼U+0800到U+D7FF以及U+E000到U+FFFF的范圍。

  1. U+0800 to U+D7FF:
  2. 11100000 E0 -- ED 11101101
  3. 10100000 A0 --9F10011111
  4. 1000000080-- BF 10111111
  5. U+E000 to U+FFFF:
  6. 11101110 EE -- EF 11101111
  7. 1000000080-- BF 10111111
  8. 1000000080-- BF 10111111‘

現在終於了4字節的這部分,這些字節的格式是11110www 10xxxxxx 10yyyyyy 10zzzzzz,我們有21個比特位可用,這樣我們可以最大達到U+10FFFF。這段區間是沒有間隔的,不過要想覆蓋剩下的這些代碼點,我們用不著使用完這整個范圍的值,因此最終的結果是這樣的:

  1. U+010000 to U+10FFFF:
  2. 11110000 F0 -- F4 11110100
  3. 1001000090--8F10001111
  4. 1000000080-- BF 10111111
  5. 1000000080-- BF 10111111

現在我們已經介紹完了所有表示UTF-8中單個字符的有效字節序列。它們是:

  1. [00-7F]
  2. [C2-DF][80-BF]
  3. E0 [A0-BF][80-BF]
  4. [E1-EC][80-BF][80-BF]
  5. ED [80-9F][80-BF]
  6. [EE-EF][80-BF][80-BF]
  7. F0 [90-BF][80-BF][80-BF]
  8. [F1-F3][80-BF][80-BF][80-BF]
  9. F4 [80-8F][80-BF][80-BF]

這些可以用一個正則來進行匹配,不過記住了正則只能在字符上進行操作,而不是字節。在Node中,我們可以使用buffer.toString('binary')將一個緩沖區轉化成一個字符串,裡面的字符則是這些字節的代碼點的字面量(比如從0到255),然後將這個字符串用正則來進行校驗。

現在我們已經理解怎麼是UTF-8了,我們也可以明白Node中到底修改了些什麼。

  1. // Prior to these releases:
  2. newBuffer('ab\ud800cd','utf8');
  3. // <Buffer 61 62 ed a0 80 63 64>
  4. // After this release:
  5. newBuffer('ab\ud800cd','utf8');
  6. // <Buffer 61 62 ef bf bd 63 64>

字符\ud800是一個代理(surrogate),沒有對應的編碼,因此它是一個無效字符。然而,JavaScript允許這個字符串存在並且不會拋出錯誤,因此Node決定這個字符串轉化成緩沖區的時候也不要報錯。不過現在這個字符被替換成了'\ufffd',也就是未知字符。為了不讓你的程序發送一個JS認為有效的字符串而對方卻拒絕承認它是一個UTF-8串,Node將它替換成了一個非代理字符,以避免下游的程序出現錯誤。當碰到奇怪的輸入的時候,我通常是建議不要去猜測程序員到底想表達什麼,但既然Unicode提供了這樣的一個代碼點,它被“用來替換掉一個在Unicode中未知的或者無法表示的字符“,這看起來也算是個不錯的選擇。

Copyright © Linux教程網 All Rights Reserved