node编码中的坑

3,720 阅读5分钟

在上篇文章Buffer(Buffer(缓冲器))中,聊了关于编码的问题。但是编码有很多小坑,今天我们聊聊坑的问题。 第一个就是BOM头的问题。 我们都知道,NodeJs是不支持gb2312编码的, 在此之前得先知道,gb2312编码中,一个汉字是由两个字节(16个位)组成。 在我们写代码的时候经常会遇到一个问题,就是我们写的代码是gbk写的(gb2312),但NodeJs是不支持的。所以读取出来的数据,不是我们想要的。

let fs = require('fs');
let path = require('path');
let result = fs.readFileSync(path.join(__dirname,'./1.txt'));//txt的内容是前端开发
console.log(result.toString());

输出的内容是乱码

用编辑器打开txt文件也是乱码
如果不对结果进行toString,得到的buffer的内容是:
通常,我们遇到不支持gbk的文件,第一反应都会重新设置编码为utf8格式。例如对txt的操作:
这时,再去获取result的值

let fs = require('fs');
let path = require('path');
let result = fs.readFileSync(path.join(__dirname,'./1.txt'));//txt的编码已经是utf8
console.log(result);

结果是

我们都知道uft8格式的文件,一个汉字3个字节,此时输出的结果却多出3个字节。因为这是unicode的原因,它会加多3个字节的前缀。这个前缀对我们来说是没有意义的。对result进行toString()转译:

console.log(result.toString())

输出结果:

这时我们就要截掉这个BOM头。

我们看看node源码,编译的时候用了stripBOM的模块,把BOM头删掉

// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(internalModule.stripBOM(content), filename);
};

我们再看看源码里stripBOM的方法

/**
 * Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
 * because the buffer-to-string conversion in `fs.readFileSync()`
 * translates it to FEFF, the UTF-16 BOM.
 */
function stripBOM(content) {
  if (content.charCodeAt(0) === 0xFEFF) {
    content = content.slice(1);
  }
  return content;
}

stripBOM拿到内容content以后,取它的第0个,判断它的第0个是不是0xFEFF,0xFEFF就是那3个前缀的字符,那3个字符是不要的,所以做了slice处理。 stripBOM方法里要求content必须得是字符串,因为它截了一个,但是我们的buffer是3个字节,所以我们要对文件传utf8的参数:

let fs = require('fs');
let path = require('path');
let result = fs.readFileSync(path.join(__dirname,'./1.txt'),'utf8');
console.log(result);

此时result的结果就是一个字符串了:

取出result的第一个字符等于0xFEFF的话,就要slice掉。

let fs = require('fs');
let path = require('path');
function stripBOM(content) {
    if (content.charCodeAt(0) === 0xFEFF) {
      content = content.slice(1);
    }
    return content;
  }
let result = fs.readFileSync(path.join(__dirname,'./1.txt'),'utf8');
result = stripBOM(result);
console.log(result);
//输出:前端开发

一般情况下,我们读取文件的时候很少会传utf8这个参数,如果不传utf8参数,该怎么去掉BOM头?(不传utf8,得到的就是buffer;传了utf8,得到的就是字符串)

/*
Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
源码已经说明,uft8中,EF BB BF表示3个字节,那么只需判断buffer的前3位是EF BB BF,就可以删掉
*/
function stripBOM(content){
  if(Buffer.isBuffer(content)){//判断是不是buffer
    if(content[0]===0xEF&&content[1]===0xBB&&content[2]===0xBF){
      return content.slice(3);
    }
    return content;
  }else{ //是string
    if(content.charCodeAt(0)===0xFEFF){
      return content.slice(1);
    }
    return content;
  }
}

iconv-lite:让node支持gb2312

我们用nodejs爬取gb2312网页的时候,会出现乱码的情况。可以用iconv-lite把gbk转化成utf8,它是第三方模块,所以需要安装包。这个包的目的就是帮助我们转化编码。 如何调用:

let iconv = require('iconv-lite');
let fs = require('fs');
let path = require('path'); 
//iconv.decode(希望解码的目标,希望按什么方式解码)
let result = fs.readFileSync(path.join(__dirname,'./2.txt'));
result = iconv.decode(result,'gbk')
console.log(result.toString())

所以,如果只想要Buffer,我们一般不传编码;如果想看这个结果是个字符串,我们就传utf8


string_decoder

string_decoder模块用于将Buffer转成对应的字符串。使用者通过调用stringDecoder.write(buffer),可以获得buffer对应的字符串。

它的特殊之处在于,当传入的buffer不完整(比如三个字节的字符,只传入了两个),内部会维护一个internal buffer将不完整的字节cache住,等到使用者再次调用stringDecoder.write(buffer)传入剩余的字节,来拼成完整的字符。

这样可以有效避免buffer不完整带来的错误,对于很多场景,比如网络请求中的包体解析等,非常有用。

入门例子

这节分别演示了decode.write(buffer)、decode.end([buffer])两个主要API的用法。

例子一:

decoder.write(buffer)调用传入了Buffer对象,相应的返回了对应的字符串你;

const StringDecoder = require('string_decoder').StringDecoder;
const decoder = new StringDecoder('utf8');

// Buffer.from('你') => <Buffer e4 bd a0>
const str = decoder.write(Buffer.from([0xe4, 0xbd, 0xa0]));
console.log(str);  // 你

例子二:

当decoder.end([buffer])被调用时,内部剩余的buffer会被一次性返回。如果此时带上buffer参数,那么相当于同时调用decoder.write(buffer)和decoder.end()。

const StringDecoder = require('string_decoder').StringDecoder;
const decoder = new StringDecoder('utf8');

// Buffer.from('你好') => <Buffer e4 bd a0 e5 a5 bd>
let str = decoder.write(Buffer.from([0xe4, 0xbd, 0xa0, 0xe5, 0xa5]));
console.log(str);  // 你

str = decoder.end(Buffer.from([0xbd]));
console.log(str);  // 好
例子:分多次写入多个字节

下面的例子,演示了分多次写入多个字节时,string_decoder模块是怎么处理的。

首先,传入了,好还差1个字节,此时,decoder.write(xx)返回你。

然后,再次调用decoder.write(Buffer.from([0xbd])),将剩余的1个字节传入,成功返回好。

const StringDecoder = require('string_decoder').StringDecoder;
const decoder = new StringDecoder('utf8');

// Buffer.from('你好') => <Buffer e4 bd a0 e5 a5 bd>
let str = decoder.write(Buffer.from([0xe4, 0xbd, 0xa0, 0xe5, 0xa5]));
console.log(str);  // 你

str = decoder.write(Buffer.from([0xbd]));
console.log(str);  // 好
let buffer = Buffer.from('前端开发');
let buff1 = buffer.slice(0,5);
let buff2 = buffer.slice(5);
let {StringDecoder} = require('string_decoder');
let sd = new StringDecoder();
console.log(sd.write(buff1).toString());
console.log(sd.write(buff2).toString());
例子:decoder.end()时,字节数不完整的处理

decoder.end(buffer)时,仅传入了好的第1个字节,此时调用decoder.end(),返回了�,对应的buffer为。

const StringDecoder = require('string_decoder').StringDecoder;

// Buffer.from('好') => <Buffer e5 a5 bd>
let decoder = new StringDecoder('utf8');
let str = decoder.end( Buffer.from([0xe5]) );
console.log(str);  // �
console.log(Buffer.from(str));  // <Buffer ef bf bd>

参考文档:

string_decoder - 字符串解码器

你应该记住的一个UTF-8字符「EF BF BD」

巧用string_decoder将buffer转成string