Buffer對象可以與字符串之間相互轉(zhuǎn)換。目前支持的字符串編碼如下:
ASCIIUTF-8UTF-16LE/UCS-2Base64BinaryHex
1. String與Buffer相互轉(zhuǎn)換
字符串轉(zhuǎn)Buffer主要通from(string, encoding)方法數(shù)完成:
var str = 'hello'
var strBuffer = Buffer.from(str) ; //encoding不傳默認(rèn)為utf-8
//<Buffer 68 65 6c 6c 6f>
buffer轉(zhuǎn)字符串 buf.toString(encoding, start, end):
strBuffer.toString()
//'hello'
strBuffer.toString(undefined,1,2)
//'e'
encoding: 轉(zhuǎn)換成字符串后的字符編碼(默認(rèn)為utf-8);
start: 轉(zhuǎn)換的起始位置;
end: 轉(zhuǎn)換的起始位置;
這三個參數(shù)實現(xiàn)整體貨局部的轉(zhuǎn)換。如果Buffer對象由多種編碼寫入,就需要在局部指定不同的編碼,才能轉(zhuǎn)換會正常的編碼。
2. Buffer不支持的編碼類型
Node的buffer對象支持的編碼類型有限,在字符串和Buffer之間轉(zhuǎn)換的類型較少。Buffer提供了一個isEncoding()函數(shù)判斷編碼是否支持轉(zhuǎn)換。
Buffer.isEncoding(encoding)
Buffer.isEncoding('utf-8') //true
在中國常用的
GBK、GB2312、BIG-5編碼都不在支持的行列中。
Buffer.isEncoding('GBK') //false
Buffer.isEncoding('GB2312') //false
Buffer.isEncoding('BIG-5') //false
對于不支持的編碼類型,可以借助Node生態(tài)圈的模塊來完成(iconv 、iconv-lite)。
var iconv = require('iconv-lite');
var str = iconv.decode(buf, 'GBK');
var buf = iconv.encode('簡單字符', 'GBK');
很多時候cmd或其他終端輸出的內(nèi)容時,經(jīng)常看到帶白色或者黑色背景的問號,那是對無法轉(zhuǎn)換的多字節(jié)輸出“?”;
如果是單字節(jié),則輸出“?”
3. Buffer的拼接
Buffer在使用場景中,通常是一段段的方式傳輸:
var fs = reuire('fs');
var rs = fs.createReadStream('test.md');
var data = '';
rs.on('data',function(chunk){
data += chunk
});
rs.on("end",function(){
console.log(data)
})
上面的代碼用于流讀取的示范,data時間中獲取的chunk對象就是Buffer對象。但是很容易將Buffer當(dāng)做字符來理解,所以在接受上面的實例時不會覺得有異常。
當(dāng)輸入流中有寬字節(jié)編碼時,問題就容易顯現(xiàn),在很多用Node開發(fā)的網(wǎng)站上看到“????”,就是多字節(jié)的轉(zhuǎn)換異常導(dǎo)致的。
data += chunk;
這句代碼中隱藏了toString()的操作,等價于:
data = data.toString() + chunk.toString();
在語境為英文的環(huán)境中,toString()不會造成任何問題。但對于寬字節(jié)的中文卻會形成問題。
var fs = require('fs');
var readStream = fs.createReadStream('test.md',{highWaterMark:11});
var data = '';
//文件讀取中事件·····
readStream.on('data', (chunk) => {
data += chunk;
console.log('讀取文件數(shù)據(jù):', chunk);
});
//文件讀取完成事件
readStream.on('end', () => {
console.log(data);
});
搭配上面代碼的測試數(shù)據(jù)為李白的《靜夜思》。下面是執(zhí)行之后輸出的內(nèi)容:
床前明??光,疑???地上霜。
舉頭???明月,低頭思??鄉(xiāng)。
4. 亂碼產(chǎn)生的原因
在使用createReadStream創(chuàng)建文件流使用了highWaterMark,將buffer的長度設(shè)為了11,因此文件流需要讀取7次才能完成:
讀取文件數(shù)據(jù): <Buffer e5 ba 8a e5 89 8d e6 98 8e e6 9c>
讀取文件數(shù)據(jù): <Buffer 88 e5 85 89 ef bc 8c e7 96 91 e6>
讀取文件數(shù)據(jù): <Buffer 98 af e5 9c b0 e4 b8 8a e9 9c 9c>
讀取文件數(shù)據(jù): <Buffer e3 80 82 0a e4 b8 be e5 a4 b4 e6>
讀取文件數(shù)據(jù): <Buffer 9c 9b e6 98 8e e6 9c 88 ef bc 8c>
讀取文件數(shù)據(jù): <Buffer e4 bd 8e e5 a4 b4 e6 80 9d e6 95>
讀取文件數(shù)據(jù): <Buffer 85 e4 b9 a1 e3 80 82>
在data += chunk;執(zhí)行了buf.toString()并默認(rèn)utf-8為編碼,中文在utf-8下占用3個字節(jié)。第一個buffer對象在輸出時,只能顯示三個字符(11/3=3余2),Buffer中剩下的2個字節(jié)(e6 9c 兩個字節(jié)無法形成中文字)將會以亂碼的形式顯示。(注意:中文標(biāo)點也算三個字節(jié)!)
在寬字節(jié)的字符轉(zhuǎn)換成buffer的過程中(n個字節(jié)代表一個字符,注意n大于1)有截取長度的可能,比如上面使用
highWaterMark截取導(dǎo)致,每三個字節(jié)代表一個中文字,但是在一個buff中字節(jié)的長度不為3n,那些無法解析的字節(jié)被拋出?。
5. setEncoding()與string_decoder()
在上面的示例中,可讀流還有一個設(shè)置編碼的方式setEncoding(),示例如下:
var rs = fs.craateReadStream('test.md', {highWaterMark:11});
rs.setEncoding('utf-8');//setEncoding
readStream.on('data', (chunk) => {
data += chunk;
console.log('讀取文件數(shù)據(jù):', chunk);
});
//文件讀取完成事件
readStream.on('end', () => {
console.log(data);
});
結(jié)果:
床前明月光,疑是地上霜。
舉頭望明月,低頭思故鄉(xiāng)。
setEncoding()方法的作用是讓data事件傳遞的不再是一個buffer對象,而是編碼后的字符串。
在這里輸出和未調(diào)用setEncoding()的輸出完全不一樣了:
- 在調(diào)用
setEncoding()時,可讀流對象在內(nèi)部設(shè)置了一個decoder對象。每次data事件都通過decoder對象進行Buffer到字符串的解碼,然后傳遞給調(diào)用者(data事件的回調(diào))。- 所以data接收的不再是buffer對象,而是編碼后的字符。
- 關(guān)鍵點還是在
setEncoding()的內(nèi)部事件中的decoder對象。
decoder對象來自于string_decoder模塊StringDecoder的實例對象。看下面的代碼:
var StringDecoder = resquier('string_decoder').StringDecoder;
var decoder = new StringDecoder();
var buf1 = Buffer.from([0xe5,0xba,0x8a,0xe5,0x89,0x8d,0xe6,0x98 ,0x8e ,0xe6 ,0x9c]);
decoder.write(buf1);
=> '床前明' //可以看到只解析了完整的字節(jié)編碼(前9個)。
var buf2 = Buffer.from([0x88 ,0xe5 ,0x85 ,0x89 ,0xef ,0xbc ,0x8c ,0xe7 ,0x96 ,0x91 ,0xe6]);
decoder.write(buf2)
=>'月光,疑' //buf1中未解析的兩個字節(jié)在這得到解析(月字)。
- 上面的代碼中,‘月’字的轉(zhuǎn)碼并沒有在兩個部分分開輸出(
上面解析的??),- StringDecoder在得到編碼后,知道寬字節(jié)字符串在UTF-8編碼下是以3個字節(jié)的方式存儲的.
- 在第一次
write()時,只輸出了9個字節(jié)轉(zhuǎn)碼形成的字符,‘月’字的前兩個字節(jié)被保留在StringDecoder的實例內(nèi)部。- 第二次
write()時,會將2個剩余的字節(jié)和后續(xù)11個字節(jié)組合,再次用3的倍數(shù)字節(jié)進行轉(zhuǎn)碼。
StringDecoder并非萬能的,他目前只能處理UTF-8、base64和UCS-2/UTF-16LE這三種編碼。它不能從根本上解決問題。