字符編解碼

轉自:https://github.com/ProtoTeam/blog/blob/master/201712/3.mdhtml

 

做者簡介:nekron 螞蟻金服·數據體驗技術團隊前端

背景

由於中文的博大精深,以及早期文件編碼的不統一,形成了如今可能碰到的文件編碼有GB2312GBkGB18030UTF-8BIG5等。由於編解碼的知識比較底層和冷門,一直以來我對這幾個編碼的認知也很膚淺,不少時候也會疑惑編碼名究竟是大寫仍是小寫,英文和數字之間是否是須要加「-」,規則究竟是誰定的等等。node

我膚淺的認知以下:git

編碼 說明
GB2312 最先的簡體中文編碼,還有海外版的HZ-GB-2312
BIG5 繁體中文編碼,主要用於臺灣地區。些繁體中文遊戲亂碼,其實都是由於BIG5編碼和GB2312編碼的錯誤使用致使
GBK 簡體+繁體,我就當它是GB2312+BIG5,非國家標準,只是中文環境內基本都遵照。後來瞭解到,K竟然是「擴展」的拼音首字母,這很中國。。。
GB18030 GB家族的新版,向下兼容,最新國家標準,如今中文軟件都理應支持的編碼格式,文件解碼的新選擇
UTF-8 不解釋了,國際化編碼標準,html如今最標準的編碼格式。

概念梳理

通過長時間的踩坑,我終於對這類知識有了必定的認知,如今把一些重要概念從新整理以下:github

首先要消化整個字符編解碼知識,先要明確兩個概念——字符集和字符編碼。web

字符集

顧名思義就是字符的集合,不一樣的字符集最直觀的區別就是字符數量不相同,常見的字符集有ASCII字符集、GB2312字符集、BIG5字符集、 GB18030字符集、Unicode字符集等。正則表達式

字符編碼

字符編碼決定了字符集到實際二進制字節的映射方式,每一種字符編碼都有本身的設計規則,例如是固定字節數仍是可變長度,此處不一一展開。windows

常提到的GB23十二、BIG五、UTF-8等,若是未特殊說明,通常語義上指的是字符編碼而不是字符集。後端

字符集和字符編碼是一對多的關係,同一字符集能夠存在多個字符編碼,典型表明是Unicode字符集下有UTF-八、UTF-16等等。瀏覽器

BOM(Byte Order Mark)

當使用windows記事本保存文件的時候,編碼方式能夠選擇ANSI(經過locale判斷,簡體中文系統下是GB家族)、Unicode、Utf-8等。

爲了清晰概念,須要指出此處的Unicode,編碼方式實際上是UTF-16LE。

有這麼多編碼方式,那文件打開的時候,windows系統是如何判斷該使用哪一種編碼方式呢?

答案是:windows(例如:簡體中文系統)在文件頭部增長了幾個字節以表示編碼方式,三個字節(0xef, 0xbb, 0xbf)表示UTF-8;兩個字節(0xff, 0xfe或者0xfe, 0xff)表示UTF-16(Unicode);無表示GB**。

值得注意的是,因爲BOM不表意,在解析文件內容的時候應該捨棄,否則會形成解析出來的內容頭部有多餘的內容。

LE(little-endian)和BE(big-endian)

這個涉及到字節相關的知識了,不是本文重點,不過提到了就順帶解釋下。LE和BE表明字節序,分別表示字節從低位/高位開始。

咱們常接觸到的CPU都是LE,因此windows裏Unicode未指明字節序時默認指的是LE。

node的Buffer API中基本都有相應的2種函數來處理LE、BE,貼個文檔以下:

const buf = Buffer.from([0, 5]);

// Prints: 5
console.log(buf.readInt16BE());

// Prints: 1280
console.log(buf.readInt16LE());

Node解碼

我第一次接觸到該類問題,使用的是node處理,當時給個人選擇有:

  • node-iconv(系統iconv的封裝)
  • iconv-lite(純js)

因爲node-iconv涉及node-gyp的build,而開發機是windows,node-gyp的環境準備以及後續的一系列安裝和構建,讓我這樣的web開發人員痛(瘋)不(狂)欲(吐)生(嘈),最後天然而然的選擇了iconv-lite。

解碼的處理大體示意以下:

const fs = require('fs') const iconv = require('iconv-lite') const buf = fs.readFileSync('/path/to/file') // 能夠先截取前幾個字節來判斷是否存在BOM buf.slice(0, 3).equals(Buffer.from([0xef, 0xbb, 0xbf])) // UTF-8 buf.slice(0, 2).equals(Buffer.from([0xff, 0xfe])) // UTF-16LE const str = iconv.decode(buf, 'gbk') // 解碼正確的判斷須要根據業務場景調整 // 此處截取前幾個字符判斷是否有中文存在來肯定是否解碼正確 // 也能夠反向判斷是否有亂碼存在來肯定是否解碼正確 // 正則表達式內常見的\u**就是unicode碼點 // 該區間是常見字符,若是有特定場景能夠根據實際狀況擴大碼點區間 /[\u4e00-\u9fa5]/.test(str.slice(0, 3)) 

前端解碼

隨着ES20151的瀏覽器實現愈來愈普及,前端編解碼也成爲了可能。之前經過form表單上傳文件至後端解析內容的流程如今基本能夠徹底由前端處理,既少了與後端的網絡交互,並且由於有界面反饋,用戶體驗上更直觀。

通常場景以下:

const file = document.querySelector('.input-file').files[0] const reader = new FileReader() reader.onload = () => { const content = reader.result } reader.onprogerss = evt => { // 讀取進度 } reader.readAsText(file, 'utf-8') // encoding可修改

fileReader支持的encoding列表,可查閱此處

這裏有一個比較有趣的現象,若是文件包含BOM,好比聲明是UTF-8編碼,那指定的encoding會無效,並且在輸出的內容中會去掉BOM部分,使用起來更方便。

若是對編碼有更高要求的控制需求,能夠轉爲輸出TypedArray:

reader.onload = () => { const buf = new Uint8Array(reader.result) // 進行更細粒度的操做 } reader.readAsArrayBuffer(file)

獲取文本內容的數據緩衝之後,能夠調用TextDecoder繼續解碼,不過須要注意的是得到的TypedArray是包含BOM的:

const decoder = new TextDecoder('gbk') const content = decoder.decode(buf)

若是文件比較大,可使用Blob的slice來進行切割:

const file = document.querySelector('.input-file').files[0] const blob = file.slice(0, 1024)

文件的換行不一樣操做系統不一致,若是須要逐行解析,須要視場景而定:

  • Linux: \n
  • Windows: \r\n
  • Mac OS: \r

**注意:**這個是各系統默認文本編輯器的規則,若是是使用其餘軟件,好比經常使用的sublime、vscode、excel等等,都是能夠自行設置換行符的,通常是\n或者\r\n。

前端編碼

可使用TextEncoder將字符串內容轉換成TypedBuffer:

const encoder = new TextEncoder()
encoder.encode(String)

值得注意的是,從Chrome 53開始,encoder只支持utf-8編碼2,官方理由是其餘編碼用的太少了。這裏有個polyfill庫,補充了移除的編碼格式。

前端生成文件

前端編碼完成後,通常都會順勢實現文件生成,示例代碼以下:

const a = document.createElement('a')
const buf = new TextEncoder()
const blob = new Blob([buf.encode('我是文本')], {
	type: 'text/plain'
})
a.download = 'file'
a.href = URL.createObjectURL(blob)
a.click()
// 主動調用釋放內存
URL.revokeObjectURL(blob)

這樣就會生成一個文件名爲file的文件,後綴由type決定。若是須要導出csv,那隻須要修改對應的MIME type:

const blob = new Blob([buf.encode('第一行,1\r\n第二行,2')], {
	type: 'text/csv'
})

通常csv都默認是由excel打開的,這時候會發現第一列的內容都是亂碼,由於excel沿用了windows判斷編碼的邏輯(上文提到),當發現無BOM時,採用GB18030編碼進行解碼而致使內容亂碼。

這時候只須要加上BOM指明編碼格式便可:

const blob = new Blob([new Uint8Array([0xef, 0xbb, 0xbf]), buf.encode('第一行,1\r\n第二行,2')], {
	type: 'text/csv'
})

// or

const blob = new Blob([buf.encode('\ufeff第一行,1\r\n第二行,2')], {
	type: 'text/csv'
})

這裏稍微說明下,由於UTF-8和UTF-16LE都屬於Unicode字符集,只是實現不一樣。因此經過必定的規則,兩種編碼能夠相互轉換,而代表UTF-16LE的BOM轉成UTF-8編碼其實就是代表UTF-8的BOM。

附:

  1. TypedArray
  2. TextEncoder
相關文章
相關標籤/搜索