web知識進階——字符編解碼

做者簡介:nekron 螞蟻金服·數據體驗技術團隊
背景 由於中文的博大精深,以及早期文件編碼的不統一,形成了如今可能碰到的文件編碼有php

GB2312

html

GBk

前端

GB18030

node

UTF-8

git

BIG5

等。由於編解碼的知識比較底層和冷門,一直以來我對這幾個編碼的認知也很膚淺,不少時候也會疑惑編碼名究竟是大寫仍是小寫,英文和數字之間是否是須要加「-」,規則究竟是誰定的等等。
我膚淺的認知以下:
編碼 說明 GB2312 最先的簡體中文編碼,還有海外版的HZ-GB-2312 BIG5 繁體中文編碼,主要用於臺灣地區。些繁體中文遊戲亂碼,其實都是由於BIG5編碼和GB2312編碼的錯誤使用致使 GBK 簡體+繁體,我就當它是GB2312+BIG5,非國家標準,只是中文環境內基本都遵照。後來瞭解到,K竟然是「擴展」的拼音首字母,這很中國。。。 GB18030 GB家族的新版,向下兼容,最新國家標準,如今中文軟件都理應支持的編碼格式,文件解碼的新選擇 UTF-8 不解釋了,國際化編碼標準,html如今最標準的編碼格式。 概念梳理 通過長時間的踩坑,我終於對這類知識有了必定的認知,如今把一些重要概念從新整理以下:
首先要消化整個字符編解碼知識,先要明確兩個概念——字符集和字符編碼。
字符集 顧名思義就是字符的集合,不一樣的字符集最直觀的區別就是字符數量不相同,常見的字符集有ASCII字符集、GB2312字符集、BIG5字符集、 GB18030字符集、Unicode字符集等。
字符編碼 字符編碼決定了字符集到實際二進制字節的映射方式,每一種字符編碼都有本身的設計規則,例如是固定字節數仍是可變長度,此處不一一展開。
常提到的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,貼個文檔以下:github

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。
解碼的處理大體示意以下:web

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:windows

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: rn
Mac OS: r
注意:這個是各系統默認文本編輯器的規則,若是是使用其餘軟件,好比經常使用的sublime、vscode、excel等等,都是能夠自行設置換行符的,通常是n或者rn。
前端編碼 可使用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。
附:
TypedArray
TextEncoder

本文介紹了字符編解碼,感興趣的同窗能夠關注專欄或者發送簡歷至'tao.qit####alibaba-inc.com'.replace('####', '@'),歡迎有志之士加入~
原文地址:https://github.com/ProtoTeam/...
轉載於猿2048:➯《web知識進階——字符編解碼》

相關文章
相關標籤/搜索