記一次由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 部份內容參考自阮老師文章

相關文章
相關標籤/搜索