3. Buffer的转换,终端的乱码的形成。

2020-01-02  本文已影响0人  萘小蒽

Buffer对象可以与字符串之间相互转换。目前支持的字符串编码如下:

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()的输出完全不一样了:

  1. 在调用setEncoding()时,可读流对象在内部设置了一个decoder对象。每次data事件都通过decoder对象进行Buffer到字符串的解码,然后传递给调用者(data事件的回调)。
  2. 所以data接收的不再是buffer对象,而是编码后的字符。
  3. 关键点还是在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这三种编码。它不能从根本上解决问题。

上一篇下一篇

猜你喜欢

热点阅读