關於字元編碼你應該知道的事情

shoyuf_發表於2019-02-19

讀完本文你將瞭解的知識點

  1. 為什麼 Windows 上使用 Notepad 會出現亂碼
  2. 為什麼 Emoji 表情在有些手機上顯示不準確
  3. 為什麼 Emoji 在沒有做過特殊優化的資料庫中儲存失敗
  4. 為什麼使用 Linux 開發的程式碼他人使用 Windows 開發後換行符全變了
  5. 為什麼在 JS 中 [...'?‍?‍?‍?'] => ["?", "‍", "?", "‍", "?", "‍", "?"]
  6. 新版本 ECMAScript 針對 JavaScript 編碼問題做了哪些改進
  7. 為什麼使用 Google Chrome 開啟 JS 檔案,檔案中的中文字元會變成亂碼

位元、位元組

  1. 位元 ( Bit / Binary digit ) 縮寫為 b,計算機最小的儲存單位,以 0/1 來表示值

  2. 位元組 ( Byte ) 縮寫為 B,8 個位元表示一個位元組

    在計算機內部,所有的資訊最終都表示為一個二進位制的序列。每一個二進位制位 ( Bit ) 有 0 和 1 兩種狀態,因此八個二進位制位就可以組合出 256 種狀態,這被稱為一個位元組 ( Byte ) ,也就是說,一個位元組一共可以用來表示 256 種不同的狀態或者符號。如果我們製作一張對應表格,對於每一個 8 位二進位制序列,都對應唯一的一個符號。每一個狀態對應一個符號,就是 256 個符號,從 0000 0000 到 1111 1111 。

ASCII 與 EASCII

  1. ASCII (American Standard Code for Information Interchange,美國資訊交換標準程式碼)

    1967 年釋出,最後更新於 1986 年,共定義了 128 ( 2⁷ ) 個字元( 0x00 - 0x7F ) ,其中 33 個字元為不可列印字元 ( 0x00 - 0x1F & 0x7F ),95 個可列印字元 ( 0x20 - 0x7E )

    可列印字元為標準鍵盤中可輸入的字元,如下所示:

    10 個數字 ( 0-9 ),26×2 個大小寫字母 ( a-z A-Z ) ,32 個標點符號 1 個空格 ( ,./;'[]\-=~!@#$%^&*()_+{}|:"<>? )

    ASCII 的侷限在於只能顯示 26 個基本拉丁字母、阿拉伯數目字和英式標點符號,因此只能用於顯示現代美國英語,而其他攜帶類似於重音符號的字母無法顯示 ( naïve、café )

  2. EASCII ( Extended ASCII,延伸美國標準資訊交換碼 )

    由於 ASCII 的天然不足,它的變種體迅速出現,相容字符集對ASCII的處理

  • ISO/IEC 646 1972年

    該標準來自數個國家標準,最主要的是美國的 ASCII 標準,ISO 646 為了表示歐洲各種語言的帶附加符號( diacritical mark )的變音字母,由於沒有碼位空間去直接編碼這些變音字母,所以用幾個標點符號來兼作變音字母的附加符號

  • ISO/IEC 8859

    擴充套件字元:0xA0 ( 160 ) - 0xFF ( 255 ) 淘汰了 ISO 646 編碼標準

    ISO 8859 統一了此前各國各語言的單獨編碼的混亂局面;廢棄了 ISO 646 使用的退格鍵開始的轉義序列來表示變音字母的方法,而是在 G1 區域直接編碼表示變音字母。

    ISO 8859 有 15 個子版本( 1-11,13-16 ),其中囊括了大部分歐州語言,英語因為沒有重音字母,所有可以使用其中任何一個子版本表示

    Microsoft Codepage 1252 為 ISO 8859-1 的超集,擴充了 0x80 - 0x9F 來編碼一些可列印字元 ( € ‚ ƒ „ … † ‡ ˆ ‰ Š ‹ Œ Ž ‘ ’ “ ” • – — ˜ ™ š › œ ž Ÿ )

    例如在中國 GB/T 1988-80 標準中: $ u+0024 替換為 ¥ u+00A5 ,~ u+007E 替換為 u+203E

  1. ANSI

    Windows 作業系統上的 ANSI 編碼並不是指的是美國國家標準學會 ( ANSI ),而是用來指稱多個不同的內碼表,比如在簡體中文編碼作業系統中,ANSI 實際使用 GB 系字元編碼

中文

  • GB2312 ( 1981 ) 6763 個漢字,最初版本,雙位元組編碼
  • GB12345 ( 1993 ) 6866 個漢字,為了適應繁體漢字資訊處理而制定的標準
  • GBK ( 1995 ) 21886個漢字和圖形符號,不屬於國家標準
  • GB18030 ( 2000 ),70244 個字元,基於 GBK,現行版本

國際通用標準

  • Unicode ( 萬國碼、國際碼、統一碼、單一碼 )

    最初版本:1.0.0 釋出,1991 年 10 月釋出,7161 個字元 當前正式版本 Unicode 11.0 ( 2018 年 6 月 ) 擁有 137374 個字元 當前最新版本:Emoji 12.0 Beta

    表示方法:

    • 基本平面:通常會用 "U+" 然後緊接著 4 個 16 進位制的數字來表示這一個字,可表示 6 萬餘個字元
    • 其他平面使用 "U-" 然後接著 8 個 16 進位制數字表示
  • ISO/IEC 10646 ( UCS / 通用字符集 )

    該字符集包括了其他所有字符集,保證了與其他字符集的雙向相容,ISO 10646 有三種實現級別,不同的實現級別能支援的字元數量不同

    與 Unicode 的關係:

  • 所有字元在相同位置且有相同名字
  • Unicode 標準裡有詳細說明某些語言和文字的表達演算法等
  • ISO 承諾,ISO 10646 將不會替超出 U+10FFFF( Unicode 編碼以 U+ 開頭) 的 UCS-4 編碼賦值

UTF ( Unicode Transformation Format )

Unicode 是一個字符集,其實現方式稱為 Unicode 轉換格式,即 UTF

  • UTF-32

    Unicode 與 UCS 合併之前已經產生了 UCS-4 編碼方式,UCS-4 使用了 32 位來表示每個編碼,為了相容 Unicode 產生了 UTF-32 標準,編碼空間限制在了 0x000000 - 0x10FFFF 之間,因此可以說 UTF-32 是 UCS-4 的子集。由於 UTF-32 的編碼空間佔用過大,因此在 HTML5 標準中明確規定不能使用 UTF-32 進行編碼

  • UTF-16

    UTF-16 編碼擁有定長和變長兩個編碼特點,對於 Unicode 基本平面的字元,UTF-16 佔用兩個位元組,對於輔助平面的字元,UTF-16 編碼佔用四個位元組

    Unicode 規範定義,每一個檔案的最前面分別加入一個表示編碼順序的字元,這個字元的名字叫做 “零寬度非換行空格 ( zero width no-break space )”,用 FE FF 表示。但在不同計算機系統中對位元組順序的理解是不一致的,即出現了大端序 ( UTF-16 BE ) 與小端序 ( UTF-16 LE ) 兩種情況。文字頭部使用 FE FF 與 FF FE 進行區分,此區分符稱為“位元組順序標記 ( BOM ) ”

    如何確定雙位元組和四位元組:

    在基本平面內,從 U+D800 到 U+DFFF 是一個空段,不對應任何碼點,這個空段用來對映輔助平面的字元,即一個輔助平面的字元,被拆成兩個基本平面的字元表示。

    例如: ? 可以表示為 U+D83D U+DC68

  • UTF-8

    由於前兩種編碼方式的編碼規則對與英語國家來說非常浪費(2-4 位元組編碼)

    UTF-8 當前使用 1-6 個位元組為每個字元編碼

  1. 對於單位元組的符號,位元組的第一位設為 0 ,後面 7 位為這個符號的 Unicode 碼。因此對於英語字母,UTF-8 編碼和 ASCII 碼是相同的。

  2. 對於n位元組的符號( n > 1 ),第一個位元組的前n位都設為 1,第 n + 1 位設為0,後面位元組的前兩位一律設為 10。剩下的沒有提及的二進位制位,全部為這個符號的 Unicode 碼。

  3. 帶有附加符號的拉丁文、希臘文、西裡爾字母、亞美尼亞語、希伯來文、阿拉伯文、敘利亞文及它拿字母則需要兩個位元組編碼 ( Unicode 範圍由 U+0080 至 U+07FF )。

  4. 其他基本多文種平面 ( BMP ) 中的字元(這包含了大部分常用字,如大部分的漢字)使用三個位元組編碼 ( Unicode範圍由U+0800至U+FFFF )。

  5. 其他極少使用的 Unicode 輔助平面的字元使用 4-6 位元組編碼

  • UCS-2

    JavaScript 採用了 Unicode 字符集。但是隻支援一種編碼方式。JS 最先採用的編碼既不是 UTF-16 也不是 UTF-32 或 UTF-8 ,而是 UCS-2 。UTF-16 明確宣佈是 UCS-2 的超集。UTF-16 中基本平面字元延用 UCS-2 編碼。輔助平面字元定義了 4 個位元組的表示方法。

    JS 只能處理 UCS-2 編碼,造成所有字元在這門語言中都是兩個位元組,如果是四個位元組的字元。會被當做兩個雙位元組的字元處理。

    兩者的關係簡單說,就是 UTF-16 取代了 UCS-2,或者說 UCS-2 整合進了 UTF-16。所以,現在只有 UTF-16,沒有 UCS-2。

碼點和平面

字元會從 0 開始為每個字元指定一個編碼, 這個編碼叫做碼點

舉例 Unicode 中給字元進行分割槽定義,每個區稱為一個面,Unicode 擁有 0-16 共 17 個平面,每個平面 16⁴ 個字元

平面 字元值 描述
0號平面 U+0000 - U+FFFF 基本多文種平面
1號平面 U+10000 - U+1FFFF 多文種補充平面
2號平面 U+20000 - U+2FFFF 表意文字補充平面
3號平面 U+30000 - U+3FFFF 表意文字第三平面(未正式使用)
4 - 13號平面 U+40000 - U+DFFFF (尚未使用)
14號平面 U+E0000 - U+EFFFF 特別用途補充平面
15號平面 U+F0000 - U+FFFFF 保留作為私人使用區(A區)
16號平面 U+100000 - U+10FFFF 保留作為私人使用區(B區)

題首問題

  • 為什麼 Windows 上使用 Notepad 會出現亂碼

    Windows 上的 Notepad 軟體在儲存檔案時預設使用的是 ANSI 編碼儲存,而在開啟的時候需要猜測 txt 檔案的編碼方式,如果文件中出現了 ANSI 編碼以外的字元,則在開啟時候可能會出現編碼識別錯誤的情況,由於 txt 檔案為純文字檔案,沒有儲存文件編碼資訊的區域,則此問題可能一直存在。

    解決該問題可在儲存檔案的時候使用 UTF-8 編碼儲存,但需要注意的是:

    Windows 的 Notepad 應用使用 UTF-8 儲存的時候實際使用的為 UTF-8 BOM 方式,其表現為在文字最開頭新增 EF BB BF ,這部分稱為 UTF-8 位元組順序標記 ,該方式並非強制標準,如果在程式碼檔案中使用該方式儲存則有可能出現執行錯誤。

  • 為什麼 Emoji 表情在有些手機上顯示不準確

    當前 iOS 12 使用的 Unicode 版本為 11,而大眾使用比較的 Android 8.0 使用的Unicode 版本為 9,如果在 Android 系統中出現了新版本的字元,則會出現無法顯示或顯示錯誤的情況。

    例如在 Unicode 8.0 中加入了 5 個菲茨帕特里克修飾符,用來調節人形表情的膚色,如果在低於此版本的 Unicode 中顯示的字元為兩個字元,分別是顏色加人偶。

    另外 Unicode 新版本中使用 U+200D 零寬連字 ( ZWJ ) 將多個 Emoji 連起來,例如 ?‍?‍?‍? => ????

  • 為什麼 Emoji 在沒有做過特殊優化的資料庫中儲存失敗

    Emoji 表情佔用 4 個位元組,但是 MySQL 資料庫使用的 utf-8 預設編碼最多隻能儲存 3 個位元組 ( UTF-8 標準支援最長編碼為 6 位元組 ),就會導致儲存不進去,在讀取的時候讀取不完整,導致亂碼

    修復方法為:修改資料庫字符集為 uft8mb4,如果資料庫連線池中對字符集作出了設定需要在連結中去掉 characterEncoding 引數

  • 為什麼使用 Linux 開發的程式碼他人使用 Windows 開發後換行符全變了

    Windows 系列系統使用的換行標誌為 CRLF,該換行標誌與 Unix/Linux 的 LF 換行及 macOS 的 CR 換行不相同。 如果在程式碼工程中使用了 Code Lint 工具自動格式化,可能會使程式碼中的 LF 換行自動轉換為 CRLF 換行,Git 中也能捕獲或忽略這個變化。 另外,從 Windows 10 1803 開始,支援 Unix/Linux 的 LF 換行及 macOS 的 CR 換行。

  • 為什麼在 JS 中 [...'?‍?‍?‍?'] => ["?", "‍", "?", "‍", "?", "‍", "?"]

    ?‍?‍?‍? 是 2015 年新增到 Emoji 2.0 中的新字元,使用 U+200D 零寬連字 (ZWJ) 將4個 Emoji 連起來,可使用以下程式碼檢測

    [...'?‍?‍?‍?'].forEach(e=>{console.log(e.codePointAt().toString(16))})

  • 新版本 ECMAScript 針對 JavaScript 編碼問題做了哪些改進

    由於 JavaScript 使用的是隻支援雙位元組編碼的 USC-2 編碼方式,所以所有超過二位元組編碼的 Unicode 字元都無法在 JavaScript 中處理

    例如 '?'.charCodeAt().toString(16) 輸出的結果為 d83d ,而?的Unicode 碼點卻不是 d83d,造成這樣的原因為 JavaScript 只處理了該字元的前兩個位元組

    為了解決這些問題,ECMAScript 6 種增強了對新版本 Unicode 的支援。 例如:

    1. for of 迴圈中對雙位元組以上字元能識別正確長度
    2. Array.from 等方法能正確劃分字串
    3. 支援直接使用碼點表示字元,例如'\ud83d\udc68' === '?' === '\u{1F468}'
    4. String.fromCodePoint()String.prototype.codePointAt() 等方法代替 String.fromCharCode()String.prototype.charCodeAt() 等方法,以用於支援 UTF-16 編碼字元
    5. 正規表示式提供了 u 修飾符,對正規表示式新增4位元組碼點的支援
    6. 提供了normalize方法,允許"Unicode正規化" ,例如:'\u01D1'.normalize() === '\u004F\u030C'.normalize()
  • 為什麼使用 Google Chrome 開啟 JS 檔案,檔案中的中文字元會變成亂碼

    由於 2017 年更新的某版本 Chrome 中,去除了對 JS 檔案預設編碼 UTF-8 的支援,使用了系統預設編碼(例如中文作業系統使用 GB18030 )對 JS 檔案的解碼,所以導致 JS 檔案中的中文字元變成亂碼。

    解決方法有兩種:

    1. 在檔案伺服器中對返回頭的 Content-Type 設定加上 charset=UTF-8
    2. 瀏覽器中使用外掛改變網頁編碼方式,例如使用 FEHelper 工具

參考資料:

本文首發地址

blog.shoyuf.top

第二次在掘金上發文章,歡迎各位評論區中吐槽指正

相關文章