字元編碼那些事兒

qianby發表於2021-09-09

每個文字編輯器都有預設的編碼方式(比如 UTF-8 編碼),當我們儲存文件的時候,可以選擇編碼方式,如果沒有特意選擇編碼方式,文字編輯器就會以預設的編碼方式將文字內容轉換為二進位制,儲存到計算機的硬碟裡。我們開啟文字編輯器,新建一篇文件,只輸入兩個漢字 你好,然後以 UTF-8 的編碼方式儲存文件,那麼硬碟裡儲存的是二進位制的資料 11100100 10111101 10100000 11100101 10100101 10111101。以上這種,將文字字元按照某種規則轉換為二進位制資料的過程,就稱之為編碼。反過來,將二進位制資料按照某種規則轉換為文字字元的過程,就稱之為解碼,比如用文字編輯器開啟一篇已經存在的文件,文字編輯器會根據文件的編碼方式,將硬碟裡的二進位制資料轉換為文字字元顯示出來。

字符集和字元編碼

我們先來看幾個概念:

  • 編碼:按照某種規則,將字元轉換為二進位制數儲存在計算機中

  • 解碼:按照某種規則,將計算機中儲存的二進位制數轉換為字元,顯示出來

  • 字符集:文字元號的集合,比如 ASCII 字符集、GB2312 字符集、Unicode 字符集

  • 字元編碼:是一套法則,將字符集轉換為二進位制數

ASCII 字符集和字元編碼

ASCII(American Standard Code for Information Interchange,美國資訊交換標準程式碼)是基於拉丁字母的一套電腦編碼系統。它主要用於顯示現代英語,而其擴充套件版本 EASCII 則可以勉強顯示其他西歐語言。它是現今最通用的單位元組編碼系統(但是有被 Unicode 追上的跡象),並等同於國際標準ISO/IEC 646。

  • ASCII 字符集:一共包括 128 個字元,主要包括控制字元(Enter鍵、退格、換行鍵等),可顯示字元(英文大小寫字元、阿拉伯數字和西文符號)。

  • ASCII 編碼:將 ASCII 字符集轉換為二進位制數的規則,使用 7 位(bits)表示一個字元。

  • EASCII 字符集:一共包括 256 個字元,在 ASCII 字符集的基礎上,擴充套件了一些歐洲常用字元。

  • EASCII 編碼:將 EASCII 字符集轉換為二進位制資料的規則,使用 8 位(bits)也就是一個位元組表示一個字元。

ASCII 字符集對映到數字編碼規則如下圖所示:

圖片描述

ascii.png

GB2312 字符集和字元編碼

計算機發明之處及後面很長一段時間,只用應用於美國及西方一些國家,ASCII 能夠很好滿足使用者的需求。但是當中國也有了計算機之後,為了顯示中文,必須設計一套編碼規則用於將漢字轉換為計算機可以接受的數字系統的數。於是在 ASCII 字符集的基礎上,擴充套件出了 GB2312 字符集。GB2312 字符集規定:一個小於 127 的字元的意義與原來相同,但兩個大於 127 的字元連在一起時,就表示一個漢字,前面的一個位元組(稱之為高位元組)從 0xA1 到 0xF7,後面一個位元組(低位元組)從 0xA1 到 0xFE,這樣我們就可以組合出大約 7000 多個簡體漢字了。在這些編碼裡,還把數學符號、羅馬希臘的 字母、日文的假名們都編進去了,連在 ASCII 裡本來就有的數字、標點、字母都統統重新編了兩個位元組長的編碼,這就是常說的 "全形" 字元,而原來在 127 號以下的那些就叫 "半形" 字元了。

  • GB2312 字符集:在 ASCII 字符集的基礎上,擴充套件了 7000 多個簡體漢字以及標點符號等字元。

  • GB2312 字元編碼:將 GB2312 字符集轉換為二進位制資料的規則,屬於 ASCII 字符集的字元佔一個位元組,中文(包括全形標點符號)佔兩個位元組。

Unicode 字符集和字元編碼

Unicode 是為了解決傳統的字元編碼方案的侷限而產生的,它為每種語言中的每個字元設定了統一併且唯一的二進位制編碼,以滿足跨語言、跨平臺進行文字轉換、處理的要求。Unicode 字符集中每個字元對應的二進位制數,又稱之為 Unicode 碼點,如果用十進位制來表示碼點,常用字元的碼點在 0-65535 範圍之內,碼點超過 65535 的字元叫做輔助平面(星芒層)。Unicode 是字符集,UTF-32 / UTF-16 / UTF-8 是三種字元編碼方案。

  • Unicode 字符集:包含世界上所有語言的文字和符號。

  • Unicode 碼點:每個字元對應一個 Unicode 編碼號,常用的在 0-65535,編碼號超過 65535 的字元叫做輔助平面(星芒層)。

  • UTF-32 編碼:每個字元都用四個位元組來儲存。

  • UTF-16 編碼:0-65535 的字元用兩個位元組儲存,輔助平面的用四個位元組儲存。

  • UTF-8 編碼:屬於 ASCII 字符集的字元用一個位元組儲存,帶有附加符號的拉丁文、希臘文、西裡爾字母、亞美尼亞語、希伯來文、阿拉伯文、敘利亞文及它拿字母需要兩個位元組,漢字需要三個位元組,輔助平面四個位元組。

JavaScript 字符集

JavaScript 使用 Unicode 字符集,使用 UTF-16 編碼方式。var foo = '你好' 這段程式碼為變數 foo 賦值了一個字串,JavaScript 引擎執行時會在記憶體中分配一部分割槽域來儲存該字串。由於記憶體中儲存的都是二進位制資料,所以需要將字串 你好 按照 UTF-16 編碼方式轉換為二進位制,所以記憶體中儲存的是 01001111 01100000 01011001 01111101。注意 UTF-16 只是 JavaScript 引擎的編碼方式,而不是 JavaScript 文件儲存時的編碼方式。如果透過文字編輯器將這段 JavaScript 程式碼所在的文件以 UTF-8 的編碼方式儲存在硬碟裡,那麼文件中 你好 這兩個字元會按照 UTF-8 編碼方式轉換為二進位制 11100100 10111101 10100000 11100101 10100101 10111101。也就是說硬碟裡儲存的是 UTF-8 編碼的二進位制資料,而記憶體裡儲存的是 UTF-16 編碼的二進位制資料。

URL 的編碼和解碼

網頁的 URL 只能包含合法的字元,這可以分成兩類。

URL 元字元:分號(;),逗號(,),斜槓(/),問號(?),冒號(:),at(@),&,等號(=),加號(+),美元符號($),井號(#)
語義字元:a-z,A-Z,0-9,連詞號(-),下劃線(_),點(.),感嘆號(!),波浪線(~),星號(*),單引號('),圓括號(())

除了以上字元,其他字元出現在 URL 之中都必須轉義,規則是根據作業系統的預設編碼,將每個位元組轉為百分號(%)加上兩個大寫的十六進位制字母。比如,UTF-8 的作業系統上,春節這個 URL 之中,漢字“春節”不是 URL 的合法字元,所以被瀏覽器自動轉成 %E6%98%A5%E8%8A%82。其中,“春”轉成了%E6%98%A5,“節”轉成了%E8%8A%82。這是因為“春”和”節“的 UTF-8 編碼分別是E6 98 A5E8 8A 82,將每個位元組前面加上百分號,就構成了 URL 編碼。

JavaScript 提供四個 URL 的編碼/解碼方法。

  • encodeURI()

  • encodeURIComponent()

  • decodeURI()

  • decodeURIComponent()

encodeURI() 會將元字元和語義字元之外的字元,都進行轉義,用於轉碼整個 URL。
encodeURIComponent() 轉碼除了語義字元之外的所有字元,即元字元也會被轉碼。所以,它不能用於轉碼整個 URL,常用於轉碼 URL 片段。
decodeURI() 是 encodeURI() 方法的逆操作。
decodeURIComponent() 是 encodeURIComponent() 方法的逆操作。

文字格式與二進位制格式

檔案一般分為文字檔案和二進位制檔案。在計算機中,資料都是以二進位制的方式儲存的,所以不管是文字檔案還是二進位制檔案,在計算機的記憶體或硬碟裡,都是以二進位制的方式儲存的。當我們讀取文字檔案時,需要按照檔案的字元編碼方式 (比如 UTF-8) 來解碼,將計算機中儲存的二進位制資料轉換為文字字元,在客戶端顯示出來。當我們讀取二進位制檔案時,需要根據不同型別二進位制檔案的編碼方式 (比如 JPEG MP3) 來解碼,將計算機中儲存的二進位制資料轉換為圖片影片等形式,在客戶端顯示出來。

網路協議也可以分為文字協議和二進位制協議。在網路傳輸中,資料都是以二進位制的方式流動的,所以網路裝置接收到的資料包都是二進位制的。當透過文字協議接收到資料包後,首先需要根據文字協議的編碼方式 (比如 UTF-8),將資料包中的二進位制資料進行解碼,轉換為文字字元,然後再進行相關操作。比如 HTTP 協議就是文字協議,當客戶端接收到伺服器響應的一段資料之後,首先將二進位制資料包轉換為文字字元,假設文字字元中有這麼一段內容 Content-type: text/html,根據 HTTP 協議的規定,這是說伺服器響應的資料型別是 HTML 文件,那麼客戶端便可以將響應的資料按照 HTML 文件格式來解析。可見,如果網路協議是文字協議,資料接收方必須先將二進位制資料轉換為文字資料之後才能得到資訊,也就是說所有的資訊都是以文字的方式來傳達的。

如果網路協議是二進位制的協議,那麼資料接收方直接根據二進位制資料便可以獲得資訊,不需要將二進位制資料轉換為文字資料。假設根據某種二進位制協議,資料中的第一個位元組代表這段資料的型別,比如 1 代表 TEXT 文字,2 代表 HTML 文件,3 代表 JPEG 圖片 ... 當客戶端接收到了一段如下的資料:00000010 10010000 00100101 ...,那麼根據第一個位元組 00000010,便知道這段資料是一篇 HTML 文件,然後客戶端會按照 HTML 文件的方式來解析。這就相當於文字協議中的 Content-type: text/html

由此可見,網路傳輸時候,二進位制協議的資料所佔的體積更小,而且不需要先將二進位制資料轉換為文字格式,因此效率更高。

編碼轉換方式

ASCII 字符集中,有一部分是不可見字元,比如 ASCII 碼位在 0-31 的字元都是不可見的。的在網路上交換資料時,比如說從 A 地傳到 B 地,往往要經過多個路由裝置,不同的裝置對字元的處理方式有一些不同,那些不可見字元就有可能被處理錯誤,這是不利於傳輸的。另外一些網路協議(比如電子郵件)或者網路裝置不能處理非 ASCII 字符集中的字元,因此我們在傳輸資料前,可以按照某種編碼方式,先將資料中的所有字元都轉換為 ASCII 字符集中的可見字元,然後再進行傳輸。常見的編碼轉換方式有 Quoted-printable 和 Base64。

Quoted-printable 編碼

簡單來說,Quoted-printable 編碼就是將每一個 8 位的位元組,轉換為三個字元。第一個字元是 "=" 號,這是固定不變的。後面二個字元是二個十六進位制數,分別代表了這個位元組前四位和後四位的數值。如果某個 8 位的位元組是可列印的 ASCII 碼字元(十進位制值從 33 到 126),那麼該位元組保持原樣不變,"="(十進位制值61)除外。下面詳細介紹 Quoted-printable 編碼的轉換規則。

資料在計算機中以二進位制儲存,Quoted-printable 編碼以 8 位的位元組為單位轉換資料。如果某個 8 位的位元組是可列印的 ASCII 碼字元(十進位制值從 33 到 126),那麼該位元組保持原樣不變, "="(十進位制值 61)除外。如果某個 8 位的位元組是不可列印字元 (十進位制值 0-31 127),或者是 "=" 號 (十進位制值 61),則該位元組需要轉換。具體轉換規則如下:將該位元組轉換為三個位元組,第一個位元組固定為 "=" 號的 ASCII 編碼 00111101;第二個位元組取原來位元組的前 4 個 bit 位,然後前面補上 4 個 0;第三個位元組取原來位元組的後 4 個 bit 位,然後前面補上 4 個 0。

舉例來說,字串 A嚴B 按照 UTF-8 編碼轉換為二進位制 01000001 11100100 10111000 10100101 01000010 儲存在計算機中。我們對該資料進行 Quoted-printable 編碼,第一個位元組 01000001 是可列印的 ASCII 字元,所以不需要改變。第二個位元組 11100100 是不可列印的 ASCII 字元,需要轉換為 00111101 00001110 00000100 (對應的 ASCII 字元為 =E4),其中 00111101 是字元 "=" 的 ASCII 編碼,而 0000111000000100 是將原來的位元組 11100100 拆開來,透過對前 4 位和後 4 位高位補 0,各形成了一個新的位元組。後面的位元組同理。因此 01000001 11100100 10111000 10100101 01000010 經過 Quoted-printable 編碼成為 00111101 00111101 00001110 00000100 00111101 00001011 00001000 00111101 00001010 00000101 01000010 (對應的 ASCII 字元為 A=E4=B8=A5B)。這樣,我們就將 UTF-8 字串 A嚴B 轉換成了 ASCII 字串 A=E4=B8=A5B,轉換後的字串中的所有字元都是 ASCII 字符集中的可列印字元。

再舉一個例子,UTF-8 字串 a=你好 會轉換為 ASCII 字串 a=3D=E4=BD=A0=E5=A5=BD,字元 "a" 保持不變,字元 "=" 轉換為 "=3D",字元 "你" 轉換為 "=E4=BD=A0",字元 "好" 轉換為 "=E5=A5=BD"。

Base64 編碼

所謂 Base64 編碼,就是說選出 64 個字元 ---- 小寫字母a-z、大寫字母A-Z、數字0-9、符號"+"、"/"(再加上作為墊字的"=",實際上是65個字元) ---- 作為一個基本字符集。然後,其他所有符號都轉換成這個字符集中的字元。具體來說,轉換方式可以分為四步。

  1. 將每三個位元組作為一組,一共是24個二進位制位。

  2. 將這24個二進位制位分為四組,每個組有6個二進位制位。

  3. 在每組前面加兩個00,擴充套件成32個二進位制位,即四個位元組。

  4. 根據下表,得到擴充套件後的每個位元組的對應符號,這就是Base64的編碼值。

圖片描述

base64.png

舉一個具體的例項,演示英語單詞Man如何轉成Base64編碼。

  1. "M"、"a"、"n"的ASCII值分別是77、97、110,對應的二進位制值是01001101、01100001、01101110,將它們連成一個24位的二進位制字串010011010110000101101110。

  2. 將這個24位的二進位制字串分成4組,每組6個二進位制位:010011、010110、000101、101110。

  3. 在每組前面加兩個00,擴充套件成32個二進位制位,即四個位元組:00010011、00010110、00000101、00101110。它們的十進位制值分別是19、22、5、46。

  4. 根據上表,得到每個值對應Base64編碼,即T、W、F、u。

如果位元組數不足三,則這樣處理:

  • 二個位元組的情況:將這二個位元組的一共16個二進位制位,按照上面的規則,轉成三組,最後一組除了前面加兩個0以外,後面也要加兩個0。這樣得到一個三位  的Base64編碼,再在末尾補上一個"="號。

    比如,"Ma"這個字串是兩個位元組,可以轉化成三組00010011、00010110、00010000以後,對應Base64值分別為T、W、E,再補上一個"="號,因此"Ma"的Base64編碼就是TWE=。

  • 一個位元組的情況:將這一個位元組的8個二進位制位,按照上面的規則轉成二組,最後一組除了前面加二個0以外,後面再加4個0。這樣得到一個二位的Base64編碼,再在末尾補上兩個"="號。

    比如,"M"這個字母是一個位元組,可以轉化為二組00010011、00010000,對應的Base64值分別為T、Q,再補上二個"="號,因此"M"的Base64編碼就是TQ==。

用 Javascript 語言進行 Base64 編碼

在 JavaScript 中,有2個函式分別用來處理解碼和編碼base64 字串:

btoa() 用於將 ASCII 字串或二進位制資料編碼為 Base64 字串,atob() 用於將 Base64 字串解碼為 ASCII 字串或二進位制資料,這兩個方法只能用於轉碼 ASCII 字符集中的字元,對於不屬於 ASCII 字符集中的字元,使用這兩個方法會報錯。

由於 JavaScript 內部的字串都以 UTF-16 的形式進行儲存的,所以如果有字元超出了 8 位 ASCII 編碼的字元範圍時,在大多數的瀏覽器中對Unicode字串呼叫 window.btoa 將會造成一個 Character Out Of Range 的異常。有 解決這個問題:

  • 第一種方法:先用 encodeURIComponent() 轉義整個字串,然後再編碼。

  • 第二種方法:先將 UTF-16 的 DOMString 轉碼為 UTF-8 的字元陣列,然後再編碼。



作者:Karmack
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3549/viewspace-2815089/,如需轉載,請註明出處,否則將追究法律責任。

相關文章