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