记一次由BOM引起的bug

bug

今天团队小伙伴给了我一个json配置文件,可以用如下替代(毕竟内容不是重点):

{
    "text": "this is a example"
}

考虑到这个json并不需要常驻,就没有用require来引用,因为node模块的缓存机制,势必会导致内存泄漏问题的发生,就采取了以下方式:

fs.readFile(`${__dirname}/y.json`, 'utf8', function(err, str) {
  if (err) {
    throw err;
  }
  try {
    const data = JSON.parse(str);
    // ...
  } catch(err) {
    throw err;
  }
});

但是诡异的事情发生了,JSON.parse竟然报错了???

Unexpected token  in JSON at position 0

此时一脸懵逼,就用了require的方式试了一下发现一点问题都没有,考虑到了团队小伙伴使用的windows,就去问了下他,得知这个jsonnotepad++写的,加上之前写php经常遇到的BOM问题,就猜测这个bug由BOM引起,将读出来的str转成Buffer来看果然开头是ef bb bf。下面先来看下今天说的这个BOM到底是个什么东西:

BOM

字节顺序标记(英语:byte-order mark,BOM)是位于码点U+FEFF的统一码字符的名称。当以UTF-16或UTF-32来将UCS/统一码字符所组成的字符串编码时,这个字符被用来标示其字节序。它常被用来当做标示文件是以UTF-8、UTF-16或UTF-32编码的记号。

说白了就是存在于文本文件的开头,标记出文件是依靠那种格式进行编码的,mac上应该不存在,但是windowsnotepad++一般会带有。大家也可以用python写一个带有BOM标记的文件,来验证这个问题:

import codecs

code = '''{
    "x": 20
}
'''

f = codecs.open('y.json', 'w', 'utf_8_sig')
f.write(code)
f.close()

了解了产生原因以及BOM到底是什么,还有一个疑惑就是为什么用require引入可以?

require json做了啥

记得require是用的fs.readFileSync同步读取的,为什么这个可以呢?猜测都是无用的,来看下node的源码,找到了这段:

Module._extensions['.json'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  try {
    module.exports = JSON.parse(internalModule.stripBOM(content));
  } catch (err) {
    err.message = filename + ': ' + err.message;
    throw err;
  }
};

看了上面的代码可以非常明了,require在读取之后,对字符串进行了去除BOM的操作,来看下internalModule.stripBOM的实现:

function stripBOM(content) {
  // 检测第一个字符是否为BOM
  if (content.charCodeAt(0) === 0xFEFF) {
    content = content.slice(1);
  }
  return content;
}

至此问题已经解决了,但是我还有一点不明白的是ef bb bfutf8的标记,为什么会转换为feff,这个不是utf16大端序的表示吗?下面就来解决这个疑惑:

Unicode与utf8

先来讲一下编码的历史,首先出现的表示字符编码为ASCII,八位二进制,可以表示出256种状态,英文用128个符号编码就可以了,但是其他的语言却无法表示,于是在一些欧洲国家,开始各自规定其表示,比如130在法语代表一个字符,俄语代表一个字符,这样造成了0-127一致,而128-255可能会千差万别;为了解决这种问题,国际组织设计提出了Unicode,一个可以容纳全世界所有语言文字的编码方案,Unicode只规定了符号的二进制代码,但是没有规定该如何存储,比如中文可能至少需要2个字节,而英文只需要一个字节即可。utf8作为一种Unicode的实现方式被广泛颚用于互联网应用中utf8明确了编码规则:

  • 对于单字节的符号,将其第一位置为0,使用后面7位进行表示,所以说英文utf8编码与ASCII码一致

  • 对于n(n > 2)个字节的符号,第一个字节的前n为都设置为1,第n+1为设为0,后面字节的前两位一律设为10,剩下的二进制位,为这个符号的Unicode

可以参见以下对照:

字符字节Unicode符号范围utf8编码方式
10000 0000 – 0000 007F0xxxxxxx
20000 0080 – 0000 07FF110xxxxx 10xxxxxx
30000 0800 – 0000 FFFF1110xxxx 10xxxxxx 10xxxxxx
40001 0000 – 0010 FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
50020 0000 – 03FF FFFF111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
60400 0000 – 7FFF FFFF1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

来看下feff转化为ef bb bffs.readFileSync进行了buffer -> string的转换,buffer的编码为utf8,而stringUnicode,根据上表计算下:

FEFF
1111111011111111

根据其范围,得出其utf8编码:

111011111011101110111111
EFBBBF

用代码来实现下Unicodeutf8的过程:

def UnicodeToUtf8(unic):
    res = list()
    if unic < 0x7F:
        res.append(hex(unic & 0x7F))
    elif unic >= 0x80 and unic <= 0x7FF:
        # 110xxxxx
        res.append(((unic >> 6) & 0x1F) | 0xC0)
        # 10xxxxxx
        res.append((unic & 0x3F) | 0x80)
    elif unic >= 0x800 and unic <= 0xFFFF:
        # 1110xxxx
        res.append(((unic >> 12) & 0x0F) | 0xE0)
        # all is 10xxxxxx
        res.append(((unic >>  6) & 0x3F) | 0x80)
        res.append((unic & 0x3F) | 0x80)
    elif unic >= 0x10000 and unic <= 0x1FFFFF:
        # 11110xxx
        res.append(((unic >> 18) & 0x07) | 0xF0)
        # all is 10xxxxxx
        res.append(((unic >> 12) & 0x3F) | 0x80)
        res.append(((unic >>  6) & 0x3F) | 0x80)
        res.append((unic & 0x3F) | 0x80)
    elif unic >= 0x200000 and unic <= 0x3FFFFFF:
        # 111110xx
        res.append(((unic >> 24) & 0x03) | 0xF8)
        # all is 10xxxxxx
        res.append(((unic >> 18) & 0x3F) | 0x80)
        res.append(((unic >> 12) & 0x3F) | 0x80)
        res.append(((unic >>  6) & 0x3F) | 0x80)
        res.append((unic & 0x3F) | 0x80)
    elif unic >= 0x4000000 and unic <= 0x7FFFFFFF:
        # 1111110x
        res.append(((unic >> 30) & 0x01) | 0xFC)
        # all is 10xxxxxx
        res.append(((unic >> 24) & 0x3F) | 0x80)
        res.append(((unic >> 18) & 0x3F) | 0x80)
        res.append(((unic >> 12) & 0x3F) | 0x80)
        res.append(((unic >>  6) & 0x3F) | 0x80)
        res.append((unic & 0x3F) | 0x80)
    return map(lambda r:hex(r), res)
# test
print UnicodeToUtf8(0xFEFF)

utf8Unicode只需要去除标志位即可,这里就不在实现。

到此,终于清楚的可以和团队小伙伴说出bug的解决方法就利用上面的stripBOM

致谢

如有错误,还请指出!

Unicode与utf8 部分内容参考自阮老师文章

    原文作者:zp1996
    原文地址: https://segmentfault.com/a/1190000012086435
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞