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这三种编码。它不能从根本上解决问题。