JS 簡單實現UTF-8編碼,Base64編碼

羅小布發表於2020-01-30

文章用JS簡單的實現UTF-8編碼和Base64編碼,閱讀本文可以瞭解Unicode 與 UTF-8 之間的轉換,瞭解Base64編碼為什麼會使資料量變長。

概要:

  • Unicode簡單瞭解
  • UTF-8編碼
  • Base64編碼
  • 總結

Unicode,ASCII,GB2312編碼集合等,類似於字典。字元的編碼,類似於字典中的字在哪一頁哪一行。當不同系統用同一本字典查同一個編碼得到的字元會一致。

如下圖:

1. Unicode簡單瞭解

wikipedia:

Unicode is a computing industry standard for the consistent encoding, representation, and handling of text expressed in most of the world's writing systems.

在創造Unicode之前各種語言有不同的編碼集合,ASCII,GB2312等也是發展過程中編碼集合,而且這些編碼集合相互衝突,給不同語言系統進行交流帶來了麻煩。因為兩種相同的字元在不同的編碼系統中可能有完全不同的意思。於是Unicode出現了,Unicode編碼集合給每個字元提供了一個唯一的數字,不論平臺,程式,語言,Unicode 字符集因此被廣泛應用。

Javascript程式是用Unicode字符集編寫的, 字串(string)中每個字元通常來自於Unicode 字符集。

Unicode 字符集類似於字典,字元就類似於字。字元的Unicode碼值,就類似於字在字典的第頁第幾行。

2. UTF-8編碼

2.1為何有了Unicode字符集還需要 一個編碼來傳輸了?

因為Unicode 編碼轉換成二進位制,是一串0,和1,傳輸個另一方的時候,需要一個規則來分割這一串0、1。

於是就出現了UTF-n 編碼們。

8bit = 1byte

資料

UTF(Universal Transformation Format,通用傳輸格式),其實就是不改變字符集中各個字元的程式碼,建立一套新的編碼方式,把字元的程式碼通過這個編碼方式對映成傳輸時的編碼,最主要的任務就是在使用Unicode字符集保持通用性的同時節約流量和硬碟空間。

儲存
Unicode是一個符號集,規定了符號的二進位制程式碼,沒有規定這個二進位制程式碼應該如何儲存(即佔用多少個位元組)所以出現了不同的儲存實現方式。
UTF-32

字元用四個位元組表示

UTF-16

字元用兩個位元組或四個位元組表示

UTF-8

一種變長的編碼方式,根據需要用1~4個位元組來表示字元,(按需傳遞節約流量和硬碟空間,因此UTF-8用的比較廣)

2.2UTF-8編碼規則

  • 對於單位元組的符號,位元組的第一位設為0,後面7位為這個符號的 Unicode 碼。
  • 對於n位元組的符號(n > 1),第一個位元組的前n位都設為1,第n+ 1位設為0,後面位元組的前兩位設為10。剩下,全部為這個符號的 Unicode 編碼。

JS 簡單實現UTF-8編碼,Base64編碼

eg: 字元羅的UTF-8編碼

用codePointAt得到了字元的Unicode 編碼,確認用幾個位元組表示,然後按照規則填充。

編碼流程:

JS 簡單實現UTF-8編碼,Base64編碼

資料補充:

ES6 提供了codePointAt()方法,能夠正確處理位元組儲存的字元,返回一個字元的碼點(Unicode 編碼)。

ES6 提供了String.fromCodePoint()方法能正確處理一個碼點(Unicode 編碼),返回碼點(Unicode 編碼)對應的字元

2.3UTF-8編碼解碼簡單實現

// 編碼
function encodeUtf8(str) {
  var bytes = []
  for (ch of str) {
    // for...of迴圈,能正確識別 32 位的 UTF-16 字元, 可以查閱資料瞭解。
    let code = ch.codePointAt(0)
    if (code >= 65536 && code <= 1114111) {// 位運算, 補齊8位
      bytes.push((code >> 18) | 0xf0)
      bytes.push(((code >> 12) & 0x3f) | 0x80)
      bytes.push(((code >> 6) & 0x3f) | 0x80)
      bytes.push((code & 0x3f) | 0x80)
    } else if (code >= 2048 && code <= 65535) {
      bytes.push((code >> 12) | 0xe0)
      bytes.push(((code >> 6) & 0x3f) | 0x80)
      bytes.push((code & 0x3f) | 0x80)
    } else if (code >= 128 && code <= 2047) {
      bytes.push((code >> 6) | 0xc0)
      bytes.push((code & 0x3f) | 0x80)
    } else {
      bytes.push(code)
    }
  }
  return bytes
}
// 補位
function padStart(str, len, prefix) {
  return ((new Array(len + 1).join(prefix)) + str).slice(-len) //  也可用 new Array(len+1).fill(0)
}
// 解碼
function decodeUtf8(str) {
  let strValue = ''
  let obStr = [...str].map((ch)=> {
    // 一位16進位制數 轉二進位制 需要四位來表示  補全位數
    return padStart(parseInt(ch,16).toString(2), 4, 0)
  }).join('').match(/\d{8}/g).map((item)=> parseInt(item,2))
  for (var i = 0; i < obStr.length; ) {
    
    let code = obStr[i]
    let code1, code2, code3, code4, hex
    // 比較 前4 位 parseInt(11110000,2) = 240 4個位元組
    if ((code & 240) == 240) {
      code1 = (code & 0x03).toString(2)
      code2 = padStart((obStr[i + 1] & 0x3f).toString(2),6, '0')
      code3 = padStart((obStr[i + 2] & 0x3f).toString(2),6, '0')
      code4 = padStart((obStr[i + 3] & 0x3f).toString(2),6, '0')
      hex = parseInt((code1 + code2 + code3 + code4),2)
      strValue = strValue + String.fromCodePoint(hex)
      i = i + 4
    } else if ((code & 224) == 224) { // 3個位元組表
      code1 = (code & 0x07).toString(2)
      code2 = padStart((obStr[i + 1] & 0x3f).toString(2),6, '0')
      code3 = padStart((obStr[i + 2]& 0x3f).toString(2),6, '0')
      hex = parseInt((code1 + code2 + code3),2)
      strValue = strValue + String.fromCodePoint(hex)
      i = i + 3
    } else if ((code & 192) == 192) {// 2個位元組表
      code1 = (code & 0x0f).toString(2)
      code2 = padStart((obStr[i + 1] & 0x3f).toString(2),6, '0')
      hex = parseInt((obStr + code2),2)
      strValue = strValue + String.fromCodePoint(hex)
      i = i + 2
    } else {
      hex = code
      strValue = strValue + String.fromCodePoint(code)
      i = i + 1
    }
  }
  return strValue
}
// byte to hex
function transferHex(bytes) {
  let s = ''
  bytes &&
    bytes.forEach(ch => {
      s = s + ch.toString(16)
    })
  return s
}
let text = "羅小步 啊哈哈 ? ssdf 34534 ASD"
let strHax = transferHex(encodeUtf8(text))
console.log(strHax)
let str = decodeUtf8(strHax)
console.log(str)

console.log("test ok?", text === str)複製程式碼

3. Base64編碼

3.1 Base64編碼規則

規則:Base64的編碼方法要求把每三個8bit的位元組轉換成四個6bit的位元組,然後把6Bit再添兩位高位0,組成四個8Bit的位元組。

如果要編碼的二進位制資料不是3的倍數,最後剩下一個或者兩個位元組Base64會在末尾補零,再在編碼的末尾加上一個或者兩個‘=’。

每個8bit 編碼成:CHARST[paresInt(8bit ,2)]

CHARTS = ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/複製程式碼

編碼流程:

3.2 Base64編碼解碼簡單實現

const CHARTS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const prefix = '='
const prefixTwo = 2
const prefixfour = 4
function padEnd(str, len, prefix) {
  return (str + (new Array(len + 1)).join(prefix)).slice(0, len)
}
function padStart(str, len, prefix) {
  return ((new Array(len + 1).join(prefix)) + str).slice(-len)
}
// 編碼
function encodeBase64(str){
  let byteStr = ''
  for(let ch of encodeUtf8(str)){ 
    byteStr = byteStr + padStart(ch.toString(2),8,0)
  }
  let rest = byteStr.length % 6 // 餘2 就是剩下了一個位元組,餘 4 就是剩下兩個位元組
  let restStr = rest === prefixTwo ? '==' :'='
  let prefixzero = rest === prefixTwo ? prefixfour: prefixTwo
  byteStr = padEnd(byteStr , byteStr.length + prefixzero,'0')
  return byteStr.match(/(\d{6})/g).map(val=>parseInt(val,2)).map(val=>CHARTS[val]).join('') + restStr;
}
// 解碼
function decodeBase64(str) {
  let matchTime = str.match(/(ha)/g)
  
  let [...restStr] = str.replace(/=/g,'')
  restStr = restStr.map((item)=> {
    let value = CHARTS.indexOf(item)
    return padStart(value.toString(2),6,0)
  }).join('').match(/(\d{8})/g).map((item)=>parseInt(item,2).toString(16)).join()
  console.log(restStr)
  return decodeUtf8(restStr)
}

let text = "羅小步 啊哈哈 ? ssdf 34534 ASD"
let strHax = encodeBase64(text)
console.log(strHax)
let str = decodeBase64(strHax)
console.log(str)

console.log("test ok?", text === str)複製程式碼

Base64的編碼方法要求把每三個8bit的位元組轉換成四個6bit的位元組,編碼會使資料量變長原來的1/3.

4. 總結

編碼方式只是一種對字符集表現的形式。文章用js 簡單的實現utf8編碼和base64編碼。程式碼實現比較粗糙,理解不準確之處,還請教正。歡迎一起討論學習。




相關文章