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
,就去问了下他,得知这个json
用notepad++
写的,加上之前写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
上应该不存在,但是windows
的notepad++
一般会带有。大家也可以用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 bf
为utf8
的标记,为什么会转换为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 bf
,fs.readFileSync
进行了buffer -> string
的转换,buffer
的编码为utf8
,而string
为Unicode
,根据上表计算下:
F | E | F | F |
---|---|---|---|
1111 | 1110 | 1111 | 1111 |
根据其范围,得出其utf8编码:
1110 | 1111 | 1011 | 1011 | 1011 | 1111 |
---|---|---|---|---|---|
E | F | B | B | B | F |
用代码来实现下Unicode
转utf8
的过程:
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)
utf8
转Unicode
只需要去除标志位即可,这里就不在实现。
到此,终于清楚的可以和团队小伙伴说出bug的解决方法就利用上面的stripBOM
致谢
如有错误,还请指出!
Unicode与utf8 部分内容参考自阮老师文章