JS 中 GBK 編碼轉字串是非常簡單的,直接呼叫 TextDecoder
即可:
const gbkBuf = new Uint8Array([196, 227, 186, 195, 49, 50, 51])
new TextDecoder('gbk').decode(gbkBuf) // "你好123"
但反過來,字串轉 GBK 編碼卻沒這麼簡單,因為 TextEncoder
無法指定字集,只能將字串轉成 UTF-8 編碼的二進位制資料。
因此業內絕大多數的解決方案都是使用第三方編碼庫,例如 iconv。由於這些庫打包了大量字集資料,體積非常可觀,即便是精簡版的 iconv-lite 也有幾百 kB,這在瀏覽器端顯然很不完美。我們希望只用幾百位元組就能解決!
查閱資料可得,GBK 其實只有兩萬多個字元,因此最簡單的辦法就是「暴力窮舉」。藉助 TextDecoder
可遍歷出每個 GBK 對應的 JS 字元,之後的編碼過程無非就是查表而已。
事實上 GBK 的編碼範圍是有規律的:
https://en.wikipedia.org/wiki/GBK_(character_encoding)
因此只需在預定範圍中遍歷,即使多花十幾行程式碼但能提高效能,也是值得的。
const ranges = [
[0xA1, 0xA9, 0xA1, 0xFE],
[0xB0, 0xF7, 0xA1, 0xFE],
[0x81, 0xA0, 0x40, 0xFE],
[0xAA, 0xFE, 0x40, 0xA0],
[0xA8, 0xA9, 0x40, 0xA0],
[0xAA, 0xAF, 0xA1, 0xFE],
[0xF8, 0xFE, 0xA1, 0xFE],
[0xA1, 0xA7, 0x40, 0xA0],
]
const codes = new Uint16Array(23940)
let i = 0
for (const [b1Begin, b1End, b2Begin, b2End] of ranges) {
for (let b2 = b2Begin; b2 <= b2End; b2++) {
if (b2 !== 0x7F) {
for (let b1 = b1Begin; b1 <= b1End; b1++) {
codes[i++] = b2 << 8 | b1
}
}
}
}
const str = new TextDecoder('gbk').decode(codes)
// 編碼表
const table = new Uint16Array(65536)
for (let i = 0; i < str.length; i++) {
table[str.charCodeAt(i)] = codes[i]
}
如果每遍歷一個 GBK 就呼叫一次 TextDecoder
,那顯然是十分低效的。因此我們將所有 GBK 集中存放在上述 codes 陣列中,最後只呼叫一次 TextDecoder
批次轉換。
這個初始化過程只需 1ms ~ 2ms,開銷非常低。
有了對映表,編碼時直接查表即可:
function stringToGbk(str) {
const buf = new Uint16Array(str.length)
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i)
buf[i] = table[code]
}
return new Uint8Array(buf.buffer)
}
stringToGbk('你好') // [196, 227, 186, 195]
輸出結果和本文開頭演示的一致。
不過上述忽略了 ASCII 範圍,如果傳入「你好123」就有問題了。由於 GBK 的 ASCII 部分是單位元組儲存的,因此編碼邏輯需調整:
function stringToGbk(str) {
const buf = new Uint8Array(str.length * 2)
let n = 0
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i)
if (code < 0x80) {
buf[n++] = code
} else {
const gbk = table[code]
buf[n++] = gbk & 0xFF
buf[n++] = gbk >> 8
}
}
return buf.subarray(0, n)
}
stringToGbk('你好123') // [196, 227, 186, 195, 49, 50, 51]
輸出結果和本文開頭演示的一致。
出於效能考慮,這裡使用 Uint8Array 而不是 Array。但 Uint8Array 長度是固定的,申請後不能改變,因此假設輸入的字串中都是非 ASCII 字元,從而確保緩衝區充足,最後返回時再擷取。(使用 subarray 參照,無需複製)
如果編碼時傳入了 GBK 不支援的字元,按上述邏輯將會變成 0 字元,因為 table 空缺位置預設為 0。而 0 本身也是 GBK 的一部分,因此並不完善。
因此我們可將 table 填充成其他值,之後查表時出現該值,可作為例外處理。
此外根據百科上科普,微軟基於 GBK 實現的 Code page 936 多一個 0x80 字碼,對應的字元是歐元符號 €
。
試了下,即使非 Windows 系統的瀏覽器也支援:
const gbkBuf = new Uint8Array([0x80])
new TextDecoder('gbk').decode(gbkBuf) // "€"
演示:https://jsbin.com/vuxawul/edit?html,output
最終實現:https://github.com/EtherDream/str2gbk
使用這種方案,幾十行程式碼幾百位元組就能實現 GBK 編碼,並且效能非常高。