背景
因?yàn)橹形牡牟┐缶?,以及早期文件編碼的不統(tǒng)一,造成了現(xiàn)在可能碰到的文件編碼有gb2312、gbk、gb18030、utf-8、big5等。因?yàn)榫幗獯a的知識比較底層和冷門,一直以來我對這幾個編碼的認(rèn)知也很膚淺,很多時(shí)候也會疑惑編碼名到底是大寫還是小寫,英文和數(shù)字之間是不是需要加“-”,規(guī)則到底是windows定的還是國家定的等等。
我膚淺的認(rèn)知如下:
| 編碼 | 說明 |
|---|---|
| gb2312 | 最早的簡體中文編碼,還有海外版的hz-gb-2312 |
| big5 | 繁體中文編碼,主要用于臺灣地區(qū)。小時(shí)候有些繁體中文游戲亂碼,都是因?yàn)閎ig5編碼和gb2312編碼的識別混亂導(dǎo)致 |
| gbk | 簡體+繁體,我就當(dāng)它是gb2312+big5,向下兼容,在解碼時(shí)我一般選擇該編碼,因?yàn)榇虻淖稚?。后來了解到,這個就是windows幫中國“好心的”擴(kuò)展了中文編碼,致使編碼庫又多了個新成員 |
| gb18030 | gb家族的新版,向下兼容,國家標(biāo)準(zhǔn),現(xiàn)在中文軟件都理應(yīng)支持的編碼格式,文件解碼的新選擇 |
| utf-8 | 不解釋了,國際化編碼標(biāo)準(zhǔn),html現(xiàn)在最標(biāo)準(zhǔn)的編碼格式。注:windows上的文本編輯器用到的utf-8是帶BOM的 |
BOM
當(dāng)使用windows記事本保存文件的時(shí)候,編碼方式可以選擇ANSI(通過locale判斷,簡體中文系統(tǒng)下是gb家族)、Unicode、UTF-8等。那文件打開的時(shí)候,系統(tǒng)是如何判斷該使用哪種編碼方式呢?
答案是:windows(例如:簡體中文系統(tǒng))在文件頭部增加了幾個字節(jié)以表示編碼方式,三個字節(jié)(0xef, 0xbb, 0xbf)表示utf8;兩個字節(jié)(0xff, 0xfe或者0xfe, 0xff)表示unicode;無表示gbk。
值得注意的是,由于BOM不表意,在解析文件內(nèi)容的時(shí)候應(yīng)該舍棄,不然會造成解析出來的內(nèi)容頭部有多余的內(nèi)容。
unicode
unicode由于設(shè)計(jì)之初的種種外因、內(nèi)因,應(yīng)用不廣,我也了解不多,就簡單說明下:
- utf系列是unicode的實(shí)現(xiàn)
- 設(shè)計(jì)強(qiáng)制使用兩個字節(jié)表示所有字符,在英文場景下造成極大的浪費(fèi)。相對的,utf-8以一個字節(jié)表示英文
- 上小節(jié)提到有兩種方式表示unicode,分別是LE和BE。這個表示字節(jié)序,分別表示字節(jié)是從低位/高位開始(因?yàn)槊總€字符都用到2個字節(jié),而且相反的順序能映射到不同的字符)。node的Buffer API中基本都有相應(yīng)的2種函數(shù)來處理LE、BE:
buf.readInt16LE(offset[, noAssert])
buf.readInt16BE(offset[, noAssert])
后端解碼
我第一次接觸到該類問題,使用的是node處理,當(dāng)時(shí)給我的選擇有node-iconv(系統(tǒng)iconv的封裝)以及iconv-lite(純js)。由于node-iconv涉及node-gyp的build,而開發(fā)機(jī)是windows,node-gyp的環(huán)境準(zhǔn)備以及后續(xù)的一系列安裝和構(gòu)建,讓我這樣的web開發(fā)人員痛(瘋)不(狂)欲(吐)生(嘈),最后自然而然的選擇了iconv-lite。
解碼的處理大致示意如下:
const fs = require('fs')
const iconv = require('iconv-lite')
const buf = fs.readFileSync('/path/to/file')
// 可以先截取前幾個字節(jié)來判斷是否存在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ù)場景調(diào)整
// 此處截取前幾個字符判斷是否有中文存在來確定是否解碼正確
// 也可以反向判斷是否有亂碼存在來確定是否解碼正確
// 正則表達(dá)式內(nèi)常見的\u**就是unicode編碼
/[\u4e00-\u9fa5]/.test(str.slice(0, 3))
前端解碼
隨著ES20151的瀏覽器實(shí)現(xiàn)越來越普及,前端編解碼也成為了可能。以前通過form表單上傳文件至后端解析的流程現(xiàn)在基本可以完全由前端處理,既少了與后端的網(wǎng)絡(luò)交互,而且因?yàn)橛薪缑?,用戶體驗(yàn)上更直觀。
一般場景如下:
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。這里有一個比較有趣的現(xiàn)象,如果文件包含BOM,比如聲明是utf-8編碼,那指定的encoding會無效,而且在輸出的內(nèi)容會去掉BOM部分,使用起來更方便。
如果對編碼有更高要求的控制需求,可以轉(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ù)解碼,不過需要注意的是獲得的TypedArray是包含BOM的:
const decoder = new TextDecoder('gbk')
const content = decoder.decode(buf)
如果文件比較大,可以使用Blob的slice來進(jìn)行切割:
const file = document.querySelector('.input-file').files[0]
const blob = file.slice(0, 1024)
文件的換行不同操作系統(tǒng)不一致,如果需要逐行解析,需要視場景而定:
- Linux: \n
- Windows: \r\n
- Mac OS: \r
注意:這個是各系統(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開始,encoder只支持utf-8編碼3,官方理由是其他編碼用的太少了。這里有個polyfill庫,補(bǔ)充了移除的編碼格式。
前端生成文件
掌握了前端編碼,一般都會順勢實(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()
// 主動調(diào)用釋放內(nèi)存
URL.revokeObjectURL(blob)
這樣就會生成一個文件名為file.txt,后綴由type決定。使用場景一般會包含導(dǎo)出csv,那只需要修改對應(yīng)的MIME type:
const blob = new Blob([buf.encode('第一行,1\r\n第二行,2')], {
type: 'text/csv'
})
一般csv都是由excel打開的,這時(shí)候發(fā)現(xiàn)第一列的內(nèi)容都是亂碼,因?yàn)閑xcel沿用了windows判斷編碼的邏輯,當(dāng)發(fā)現(xiàn)無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'
})
這里針對第二種寫法稍微說明下,上文說過utf-8編碼是unicode編碼的實(shí)現(xiàn),所以通過一定的規(guī)則,unicode編碼都可以轉(zhuǎn)為utf-8編碼。而表明unicode的BOM轉(zhuǎn)成utf-8編碼其實(shí)就是表明utf-8的BOM。
附: