背景
因?yàn)橹形牡牟┐缶钇禄牛约霸缙谖募幋a的不統(tǒng)一,造成了現(xiàn)在可能碰到的文件編碼有gb2312
藻三、gbk
洪橘、gb18030
、utf-8
趴酣、big5
等梨树。因?yàn)榫幗獯a的知識(shí)比較底層和冷門(mén),一直以來(lái)我對(duì)這幾個(gè)編碼的認(rèn)知也很膚淺岖寞,很多時(shí)候也會(huì)疑惑編碼名到底是大寫(xiě)還是小寫(xiě)抡四,英文和數(shù)字之間是不是需要加“-”,規(guī)則到底是windows定的還是國(guó)家定的等等仗谆。
我膚淺的認(rèn)知如下:
編碼 | 說(shuō)明 |
---|---|
gb2312 | 最早的簡(jiǎn)體中文編碼指巡,還有海外版的hz-gb-2312 |
big5 | 繁體中文編碼,主要用于臺(tái)灣地區(qū)隶垮。小時(shí)候有些繁體中文游戲亂碼藻雪,都是因?yàn)閎ig5編碼和gb2312編碼的識(shí)別混亂導(dǎo)致 |
gbk | 簡(jiǎn)體+繁體,我就當(dāng)它是gb2312+big5狸吞,向下兼容勉耀,在解碼時(shí)我一般選擇該編碼,因?yàn)榇虻淖稚偬F:髞?lái)了解到便斥,這個(gè)就是windows幫中國(guó)“好心的”擴(kuò)展了中文編碼,致使編碼庫(kù)又多了個(gè)新成員 |
gb18030 | gb家族的新版威始,向下兼容枢纠,國(guó)家標(biāo)準(zhǔn),現(xiàn)在中文軟件都理應(yīng)支持的編碼格式黎棠,文件解碼的新選擇 |
utf-8 | 不解釋了晋渺,國(guó)際化編碼標(biāo)準(zhǔn)镰绎,html現(xiàn)在最標(biāo)準(zhǔn)的編碼格式。注:windows上的文本編輯器用到的utf-8是帶BOM的 |
BOM
當(dāng)使用windows記事本保存文件的時(shí)候木西,編碼方式可以選擇ANSI(通過(guò)locale判斷畴栖,簡(jiǎn)體中文系統(tǒng)下是gb家族)、Unicode八千、UTF-8等驶臊。那文件打開(kāi)的時(shí)候,系統(tǒng)是如何判斷該使用哪種編碼方式呢叼丑?
答案是:windows(例如:簡(jiǎn)體中文系統(tǒng))在文件頭部增加了幾個(gè)字節(jié)以表示編碼方式,三個(gè)字節(jié)(0xef, 0xbb, 0xbf)表示utf8扛门;兩個(gè)字節(jié)(0xff, 0xfe或者0xfe, 0xff)表示unicode鸠信;無(wú)表示gbk。
值得注意的是论寨,由于BOM不表意星立,在解析文件內(nèi)容的時(shí)候應(yīng)該舍棄,不然會(huì)造成解析出來(lái)的內(nèi)容頭部有多余的內(nèi)容葬凳。
unicode
unicode由于設(shè)計(jì)之初的種種外因绰垂、內(nèi)因,應(yīng)用不廣火焰,我也了解不多劲装,就簡(jiǎn)單說(shuō)明下:
- utf系列是unicode的實(shí)現(xiàn)
- 設(shè)計(jì)強(qiáng)制使用兩個(gè)字節(jié)表示所有字符,在英文場(chǎng)景下造成極大的浪費(fèi)昌简。相對(duì)的占业,utf-8以一個(gè)字節(jié)表示英文
- 上小節(jié)提到有兩種方式表示unicode,分別是LE和BE纯赎。這個(gè)表示字節(jié)序谦疾,分別表示字節(jié)是從低位/高位開(kāi)始(因?yàn)槊總€(gè)字符都用到2個(gè)字節(jié),而且相反的順序能映射到不同的字符)犬金。node的Buffer API中基本都有相應(yīng)的2種函數(shù)來(lái)處理LE念恍、BE:
buf.readInt16LE(offset[, noAssert])
buf.readInt16BE(offset[, noAssert])
后端解碼
我第一次接觸到該類(lèi)問(wèn)題,使用的是node處理晚顷,當(dāng)時(shí)給我的選擇有node-iconv(系統(tǒng)iconv的封裝)以及iconv-lite(純js)峰伙。由于node-iconv涉及node-gyp的build,而開(kāi)發(fā)機(jī)是windows音同,node-gyp的環(huán)境準(zhǔn)備以及后續(xù)的一系列安裝和構(gòu)建词爬,讓我這樣的web開(kāi)發(fā)人員痛(瘋)不(狂)欲(吐)生(嘈),最后自然而然的選擇了iconv-lite权均。
解碼的處理大致示意如下:
const fs = require('fs')
const iconv = require('iconv-lite')
const buf = fs.readFileSync('/path/to/file')
// 可以先截取前幾個(gè)字節(jié)來(lái)判斷是否存在BOM
buf.slice(0, 3).equals(Buffer.from([0xef, 0xbb, 0xbf])) // utf8
buf.slice(0, 2).equals(Buffer.from([0xff, 0xfe])) // unicode
const str = iconv.decode(buf, 'gbk')
// 解碼正確的判斷需要根據(jù)業(yè)務(wù)場(chǎng)景調(diào)整
// 此處截取前幾個(gè)字符判斷是否有中文存在來(lái)確定是否解碼正確
// 也可以反向判斷是否有亂碼存在來(lái)確定是否解碼正確
// 正則表達(dá)式內(nèi)常見(jiàn)的\u**就是unicode編碼
/[\u4e00-\u9fa5]/.test(str.slice(0, 3))
前端解碼
隨著ES20151的瀏覽器實(shí)現(xiàn)越來(lái)越普及顿膨,前端編解碼也成為了可能锅锨。以前通過(guò)form表單上傳文件至后端解析的流程現(xiàn)在基本可以完全由前端處理,既少了與后端的網(wǎng)絡(luò)交互恋沃,而且因?yàn)橛薪缑姹馗悖脩?hù)體驗(yàn)上更直觀。
一般場(chǎng)景如下:
const file = document.querySelector('.input-file').files[0]
const reader = new FileReader()
reader.onload = () => {
const content = reader.result
}
reader.onprogerss = evt => {
// 讀取進(jìn)度
}
reader.readAsText(file, 'utf-8') // encoding可修改
支持的encoding列表2囊咏。這里有一個(gè)比較有趣的現(xiàn)象恕洲,如果文件包含BOM,比如聲明是utf-8編碼梅割,那指定的encoding會(huì)無(wú)效霜第,而且在輸出的內(nèi)容會(huì)去掉BOM部分,使用起來(lái)更方便户辞。
如果對(duì)編碼有更高要求的控制需求泌类,可以轉(zhuǎn)為輸出TypedArray:
reader.onload = () => {
const buf = new Uint8Array(reader.result)
// 進(jìn)行更細(xì)粒度的操作
}
reader.readAsArrayBuffer(file)
獲取文本內(nèi)容的數(shù)據(jù)緩沖以后,可以調(diào)用TextDecoder繼續(xù)解碼底燎,不過(guò)需要注意的是獲得的TypedArray是包含BOM的:
const decoder = new TextDecoder('gbk')
const content = decoder.decode(buf)
如果文件比較大刃榨,可以使用Blob的slice來(lái)進(jìn)行切割:
const file = document.querySelector('.input-file').files[0]
const blob = file.slice(0, 1024)
文件的換行不同操作系統(tǒng)不一致,如果需要逐行解析双仍,需要視場(chǎng)景而定:
- Linux: \n
- Windows: \r\n
- Mac OS: \r
注意:這個(gè)是各系統(tǒng)默認(rèn)文本編輯器的規(guī)則枢希,如果是使用其他軟件,比如常用的sublime朱沃、vscode苞轿、excel等等,都是可以自行設(shè)置換行符的逗物,一般是\n或者\(yùn)r\n呕屎。
前端編碼
可以使用TextEncoder將字符串內(nèi)容轉(zhuǎn)換成TypedBuffer:
const encoder = new TextEncoder()
encoder.encode(String)
值得注意的是,從Chrome 53開(kāi)始敬察,encoder只支持utf-8編碼3秀睛,官方理由是其他編碼用的太少了。這里有個(gè)polyfill庫(kù)莲祸,補(bǔ)充了移除的編碼格式蹂安。
前端生成文件
掌握了前端編碼,一般都會(huì)順勢(shì)實(shí)現(xiàn)文件生成:
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()
// 主動(dòng)調(diào)用釋放內(nèi)存
URL.revokeObjectURL(blob)
這樣就會(huì)生成一個(gè)文件名為file.txt锐帜,后綴由type決定田盈。使用場(chǎng)景一般會(huì)包含導(dǎo)出csv,那只需要修改對(duì)應(yīng)的MIME type:
const blob = new Blob([buf.encode('第一行,1\r\n第二行,2')], {
type: 'text/csv'
})
一般csv都是由excel打開(kāi)的缴阎,這時(shí)候發(fā)現(xiàn)第一列的內(nèi)容都是亂碼允瞧,因?yàn)閑xcel沿用了windows判斷編碼的邏輯,當(dāng)發(fā)現(xiàn)無(wú)BOM時(shí),采用gb18030編碼進(jìn)行解碼而導(dǎo)致內(nèi)容亂碼述暂,這時(shí)候只需要加上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'
})
這里針對(duì)第二種寫(xiě)法稍微說(shuō)明下痹升,上文說(shuō)過(guò)utf-8編碼是unicode編碼的實(shí)現(xiàn),所以通過(guò)一定的規(guī)則畦韭,unicode編碼都可以轉(zhuǎn)為utf-8編碼疼蛾。而表明unicode的BOM轉(zhuǎn)成utf-8編碼其實(shí)就是表明utf-8的BOM。
附: