资讯专栏INFORMATION COLUMN

记一次由BOM引起的bug

cc17 / 1112人阅读

摘要:今天团队小伙伴给了我一个配置文件,可以用如下替代毕竟内容不是重点考虑到这个并不需要常驻,就没有用来引用,因为模块的缓存机制,势必会导致内存泄漏问题的发生,就采取了以下方式但是诡异的事情发生了,竟然报错了此时一脸懵逼,就用了的方式试了一下发现

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编码方式
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 部分内容参考自阮老师文章

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/89907.html

相关文章

  • 一次线上bug处理-mybatis一级缓存引起

    摘要:问题线上定时任务计算出的金额不对定位问题查看日志好像也执行了但是金额为什么和数据库的表里的不一致再查整个的定时任务日志日切日期 问题: 线上riskProvision定时任务,计算出的金额不对 定位问题: 查看日志 4.13 riskProvision 2017-04-13 01:10:00.009 [org.springframework.scheduling.quartz....

    sean 评论0 收藏0
  • 一次低级并严重开发失误

    摘要:而这一次的项目,原本以为开发挺顺利的,但是开发完了,才发现自己犯了一个低级而严重的错,这样的一个失误,我一直耿耿于怀。但是监听用户退出页面微信浏览器上面的那个返回或者关闭按钮却死活不行。也容易犯一些低级的错误。 1.前言 前端从事了超过两年,修复了无数的bug,写了无数的bug;挖了很多次坑,填了很多次坑;犯了很多次错,弥补了很多次,学习了很多次。一般而言,对于bug、坑,都是修复完了...

    wudengzan 评论0 收藏0
  • 一次绘图框架技术选型: jsPlumb VS mxGraph

    摘要:公司项目需要用到绘图框架,绘图部分以前是另一位同事负责,用的是框架。基于以上提及到的种种原因,上年年末我做起了技术调研,希望能找到一个合适我们项目的绘图框架。兼容性问题项目对浏览器兼容性比较宽松,浏览器兼容性问题不在考虑范围之内。 showImg(https://ws3.sinaimg.cn/large/006tKfTcgy1g0ppk2kkhxj30ka0b4gm5.jpg); 公司...

    longmon 评论0 收藏0
  • 一次绘图框架技术选型: jsPlumb VS mxGraph

    摘要:公司项目需要用到绘图框架,绘图部分以前是另一位同事负责,用的是框架。基于以上提及到的种种原因,上年年末我做起了技术调研,希望能找到一个合适我们项目的绘图框架。兼容性问题项目对浏览器兼容性比较宽松,浏览器兼容性问题不在考虑范围之内。 showImg(https://ws3.sinaimg.cn/large/006tKfTcgy1g0ppk2kkhxj30ka0b4gm5.jpg); 公司...

    channg 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<