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

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

  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

第二次在 segmentfault 上發文章,歡迎各位評論區中吐槽指正
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章