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

螞蟻金服資料體驗技術發表於2017-12-15

作者簡介:nekron 螞蟻金服·資料體驗技術團隊

背景

因為中文的博大精深,以及早期檔案編碼的不統一,造成了現在可能碰到的檔案編碼有GB2312GBkGB18030UTF-8BIG5等。因為編解碼的知識比較底層和冷門,一直以來我對這幾個編碼的認知也很膚淺,很多時候也會疑惑編碼名到底是大寫還是小寫,英文和數字之間是不是需要加“-”,規則到底是誰定的等等。

我膚淺的認知如下:

編碼 說明
GB2312 最早的簡體中文編碼,還有海外版的HZ-GB-2312
BIG5 繁體中文編碼,主要用於臺灣地區。些繁體中文遊戲亂碼,其實都是因為BIG5編碼和GB2312編碼的錯誤使用導致
GBK 簡體+繁體,我就當它是GB2312+BIG5,非國家標準,只是中文環境內基本都遵守。後來瞭解到,K居然是“擴充套件”的拼音首字母,這很中國。。。
GB18030 GB家族的新版,向下相容,最新國家標準,現在中文軟體都理應支援的編碼格式,檔案解碼的新選擇
UTF-8 不解釋了,國際化編碼標準,html現在最標準的編碼格式。

概念梳理

經過長時間的踩坑,我終於對這類知識有了一定的認知,現在把一些重要概念重新整理如下:

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

字符集

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

字元編碼

字元編碼決定了字符集到實際二進位制位元組的對映方式,每一種字元編碼都有自己的設計規則,例如是固定位元組數還是可變長度,此處不一一展開。

常提到的GB2312、BIG5、UTF-8等,如果未特殊說明,一般語義上指的是字元編碼而不是字符集。

字符集和字元編碼是一對多的關係,同一字符集可以存在多個字元編碼,典型代表是Unicode字符集下有UTF-8、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

本文介紹了字元編解碼,感興趣的同學可以關注專欄或者傳送簡歷至'tao.qit####alibaba-inc.com'.replace('####', '@'),歡迎有志之士加入~

原文地址:github.com/ProtoTeam/b…

相關文章