node編碼中的坑

MoTong發表於2019-03-03

在上篇文章Buffer(Buffer(緩衝器))中,聊了關於編碼的問題。但是編碼有很多小坑,今天我們聊聊坑的問題。 第一個就是BOM頭的問題。 我們都知道,NodeJs是不支援gb2312編碼的, 在此之前得先知道,gb2312編碼中,一個漢字是由兩個位元組(16個位)組成。 在我們寫程式碼的時候經常會遇到一個問題,就是我們寫的程式碼是gbk寫的(gb2312),但NodeJs是不支援的。所以讀取出來的資料,不是我們想要的。

let fs = require('fs');
let path = require('path');
let result = fs.readFileSync(path.join(__dirname,'./1.txt'));//txt的內容是前端開發
console.log(result.toString());
複製程式碼

輸出的內容是亂碼

node編碼中的坑
用編輯器開啟txt檔案也是亂碼
node編碼中的坑
如果不對結果進行toString,得到的buffer的內容是:
node編碼中的坑
通常,我們遇到不支援gbk的檔案,第一反應都會重新設定編碼為utf8格式。例如對txt的操作:
node編碼中的坑
這時,再去獲取result的值

let fs = require('fs');
let path = require('path');
let result = fs.readFileSync(path.join(__dirname,'./1.txt'));//txt的編碼已經是utf8
console.log(result);
複製程式碼

結果是

node編碼中的坑
我們都知道uft8格式的檔案,一個漢字3個位元組,此時輸出的結果卻多出3個位元組。因為這是unicode的原因,它會加多3個位元組的字首。這個字首對我們來說是沒有意義的。對result進行toString()轉譯:

console.log(result.toString())
複製程式碼

輸出結果:

node編碼中的坑
這時我們就要截掉這個BOM頭。

我們看看node原始碼,編譯的時候用了stripBOM的模組,把BOM頭刪掉

// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(internalModule.stripBOM(content), filename);
};
複製程式碼

我們再看看原始碼裡stripBOM的方法

/**
 * Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
 * because the buffer-to-string conversion in `fs.readFileSync()`
 * translates it to FEFF, the UTF-16 BOM.
 */
function stripBOM(content) {
  if (content.charCodeAt(0) === 0xFEFF) {
    content = content.slice(1);
  }
  return content;
}
複製程式碼

stripBOM拿到內容content以後,取它的第0個,判斷它的第0個是不是0xFEFF,0xFEFF就是那3個字首的字元,那3個字元是不要的,所以做了slice處理。 stripBOM方法裡要求content必須得是字串,因為它截了一個,但是我們的buffer是3個位元組,所以我們要對檔案傳utf8的引數:

let fs = require('fs');
let path = require('path');
let result = fs.readFileSync(path.join(__dirname,'./1.txt'),'utf8');
console.log(result);
複製程式碼

此時result的結果就是一個字串了:

node編碼中的坑
取出result的第一個字元等於0xFEFF的話,就要slice掉。

let fs = require('fs');
let path = require('path');
function stripBOM(content) {
    if (content.charCodeAt(0) === 0xFEFF) {
      content = content.slice(1);
    }
    return content;
  }
let result = fs.readFileSync(path.join(__dirname,'./1.txt'),'utf8');
result = stripBOM(result);
console.log(result);
//輸出:前端開發
複製程式碼

一般情況下,我們讀取檔案的時候很少會傳utf8這個引數,如果不傳utf8引數,該怎麼去掉BOM頭?(不傳utf8,得到的就是buffer;傳了utf8,得到的就是字串)

/*
Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
原始碼已經說明,uft8中,EF BB BF表示3個位元組,那麼只需判斷buffer的前3位是EF BB BF,就可以刪掉
*/
function stripBOM(content){
  if(Buffer.isBuffer(content)){//判斷是不是buffer
    if(content[0]===0xEF&&content[1]===0xBB&&content[2]===0xBF){
      return content.slice(3);
    }
    return content;
  }else{ //是string
    if(content.charCodeAt(0)===0xFEFF){
      return content.slice(1);
    }
    return content;
  }
}
複製程式碼

iconv-lite:讓node支援gb2312

我們用nodejs爬取gb2312網頁的時候,會出現亂碼的情況。可以用iconv-lite把gbk轉化成utf8,它是第三方模組,所以需要安裝包。這個包的目的就是幫助我們轉化編碼。 如何呼叫:

let iconv = require('iconv-lite');
let fs = require('fs');
let path = require('path'); 
//iconv.decode(希望解碼的目標,希望按什麼方式解碼)
let result = fs.readFileSync(path.join(__dirname,'./2.txt'));
result = iconv.decode(result,'gbk')
console.log(result.toString())
複製程式碼

所以,如果只想要Buffer,我們一般不傳編碼;如果想看這個結果是個字串,我們就傳utf8


string_decoder

string_decoder模組用於將Buffer轉成對應的字串。使用者通過呼叫stringDecoder.write(buffer),可以獲得buffer對應的字串。

它的特殊之處在於,當傳入的buffer不完整(比如三個位元組的字元,只傳入了兩個),內部會維護一個internal buffer將不完整的位元組cache住,等到使用者再次呼叫stringDecoder.write(buffer)傳入剩餘的位元組,來拼成完整的字元。

這樣可以有效避免buffer不完整帶來的錯誤,對於很多場景,比如網路請求中的包體解析等,非常有用。

入門例子

這節分別演示了decode.write(buffer)、decode.end([buffer])兩個主要API的用法。

例子一:

decoder.write(buffer)呼叫傳入了Buffer物件,相應的返回了對應的字串你;

const StringDecoder = require('string_decoder').StringDecoder;
const decoder = new StringDecoder('utf8');

// Buffer.from('你') => <Buffer e4 bd a0>
const str = decoder.write(Buffer.from([0xe4, 0xbd, 0xa0]));
console.log(str);  // 你
複製程式碼

例子二:

當decoder.end([buffer])被呼叫時,內部剩餘的buffer會被一次性返回。如果此時帶上buffer引數,那麼相當於同時呼叫decoder.write(buffer)和decoder.end()。

const StringDecoder = require('string_decoder').StringDecoder;
const decoder = new StringDecoder('utf8');

// Buffer.from('你好') => <Buffer e4 bd a0 e5 a5 bd>
let str = decoder.write(Buffer.from([0xe4, 0xbd, 0xa0, 0xe5, 0xa5]));
console.log(str);  // 你

str = decoder.end(Buffer.from([0xbd]));
console.log(str);  // 好
複製程式碼
例子:分多次寫入多個位元組

下面的例子,演示了分多次寫入多個位元組時,string_decoder模組是怎麼處理的。

首先,傳入了,好還差1個位元組,此時,decoder.write(xx)返回你。

然後,再次呼叫decoder.write(Buffer.from([0xbd])),將剩餘的1個位元組傳入,成功返回好。

const StringDecoder = require('string_decoder').StringDecoder;
const decoder = new StringDecoder('utf8');

// Buffer.from('你好') => <Buffer e4 bd a0 e5 a5 bd>
let str = decoder.write(Buffer.from([0xe4, 0xbd, 0xa0, 0xe5, 0xa5]));
console.log(str);  // 你

str = decoder.write(Buffer.from([0xbd]));
console.log(str);  // 好
複製程式碼
let buffer = Buffer.from('前端開發');
let buff1 = buffer.slice(0,5);
let buff2 = buffer.slice(5);
let {StringDecoder} = require('string_decoder');
let sd = new StringDecoder();
console.log(sd.write(buff1).toString());
console.log(sd.write(buff2).toString());
複製程式碼
例子:decoder.end()時,位元組數不完整的處理

decoder.end(buffer)時,僅傳入了好的第1個位元組,此時呼叫decoder.end(),返回了�,對應的buffer為。

const StringDecoder = require('string_decoder').StringDecoder;

// Buffer.from('好') => <Buffer e5 a5 bd>
let decoder = new StringDecoder('utf8');
let str = decoder.end( Buffer.from([0xe5]) );
console.log(str);  // �
console.log(Buffer.from(str));  // <Buffer ef bf bd>
複製程式碼

參考文件:

string_decoder - 字串解碼器

你應該記住的一個UTF-8字元「EF BF BD」

巧用string_decoder將buffer轉成string

相關文章