美文网首页
3. Buffer的转换,终端的乱码的形成。

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

作者: 萘小蒽 | 来源:发表于2020-01-02 00:12 被阅读0次

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

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

    相关文章

      网友评论

          本文标题:3. Buffer的转换,终端的乱码的形成。

          本文链接:https://www.haomeiwen.com/subject/zcmanctx.html