记一次由BOM引发的bug

bug

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

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

考虑到这个json并不须要常驻,就没有用require来引用,由于node模块的缓存机制,势必会致使内存泄漏问题的发生,就采起了如下方式:html

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居然报错了???node

Unexpected token  in JSON at position 0

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

BOM

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

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

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的源码,找到了这段:app

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的实现:ui

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

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

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编码方式
1 0000 0000 - 0000 007F 0xxxxxxx
2 0000 0080 - 0000 07FF 110xxxxx 10xxxxxx
3 0000 0800 - 0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
4 0001 0000 - 0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
5 0020 0000 - 03FF FFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
6 0400 0000 - 7FFF FFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

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

F E F F
1111 1110 1111 1111

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

1110 1111 1011 1011 1011 1111
E F B B B F

用代码来实现下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 部份内容参考自阮老师文章

相关文章
相关标签/搜索