2022年9月6日 星期二

Python 學習筆記 : 文字檔的 BOM 問題

這個 BOM 問題是我在母校圖書館借的 "Python 零基礎入門班 (碁峰 2018, 文淵閣工作室) " 這本書中看到的, 裡面提到在 Windows 系統下將檔案存成 utf-8 編碼時, 可以選擇在檔案最前方添加一個 BOM (Byte Order Mark, 位元組順序記號, 或端序記號) 字元, 用來讓軟體辨識該文件是否為 Unicode. 關於 BOM 有一篇文章講得非常詳細, 參考 :


摘要整理如下 :

BOM 存在的原因來自於 CPU 的資料寬度從最早的 8 位元開始一直倍增至目前的 64 位元, 但外部儲存媒體 (DRAM/SSD/硬碟) 卻一直都是 8 位元為一個單位, 所以 CPU 要將資料儲存到外部時, 有大端序 (Big-endian) 與小端序 (Little-endian) 兩種方式 :
  • 大端序 :
    CPU 將資料位元組由 LSB 開始往 MSB 依序寫入到記憶體時, LSB 會放在高位址, 而 MSB 會放在低位址 (小 -> 大對應高 -> 低).
  • 小端序 : 
    CPU 將資料位元組由 LSB 開始往 MSB 依序寫入到記憶體時, LSB 會放在低位址, 而 MSB 會放在高位址 (小 -> 大對應低 -> 高).
目前除 TCP/IP 採用大端序外, 大部分的軟體或協定採用小端序. 其實端序問題只是資料 I/O 時的順序問題而已. 但這與文字編碼就很有關係, 因為在 8 位元編碼的 ASCII 時代, 字元剛好可用一個 byte 表示, 因此沒有 I/O 時的順序問題, 但 Unicode 需要 16/32 位元來表示全世界文字, 於是就有了外部儲存的端序問題. Unicode 針對不同語言文字定義的特殊字元表叫做 Code Page, Windows 上預設的繁體中文用的是從 Big5 碼改編的 Code Page 950

Unicode 編碼範圍是 \u0000~\u10FFFF, 為了讓資料可以在採用不同端序的 CPU 之間交換便有了 BOM 的設計, 這是在檔案頭添加一個 0xFEFF 字元 (十進位是 65279), 此字元放入外部儲存中的順序會因為 CPU 使用的端序模式而不同, 讀取此檔案之 CPU 就可以根據此字元之排列判斷其端序是否與本機相同, 若不同就將剛低位元組順序顛倒, 這樣即可正確解碼 .16 位元與 32 為元 CPU 的 BOM 如下 : 
  • UTF-16-LE (小端序) : FF FE
  • UTF-16-BE (大端序) : FE FF
  • UTF-32-LE (小端序) : FF FE 00 00
  • UTF-32-BE (大端序) : 00 00 FE FF
UTF-8 是將以 Unicode 編碼的字串依照其編碼規則轉成 8 位元序列, 所有 CPU 讀取時都是依位址由小到大順序讀取, 故不存在端序問題, 其 BOM 編碼為 EF BB BF, 這是 0xFEFF 字元依據 UTF-8 編碼規則得到的結果, 事實上它的值仍然是 0xFEFF. 

Python 在處理 UTF-8 檔案時會自動去除與添加 BOM, 但也透過 encoding 參數指定 "utf-8-sig" 來保留字行處理 BOM 的功能. 如下面測試所示. 

首先開啟記事本, 輸入一段文字, 點選 "檔案/另存新檔" :




檔案名稱可取名為 file_without-bom.txt, 編碼維持預設的 UTF-8 (表示不加 BOM 字元) :




然後用下列程式讀取檔案內容 : 

>>> with open('file_without_bom.txt', 'r', encoding='utf-8') as f:     
    doc=f.readlines()  
    print(doc)   
    
['Hello World! \n', '你是在說哈囉嗎?']

可見讀取到的內容並無 BOM 字元. 

將上面這個檔案另存新檔為 file_with_bom.txt, 編碼則選擇 '具有 BOM 的 UTF-8' (表示要在檔頭添加 BOM 字元) : 




儲存好後會看到記事本右下角顯示 "具有 BOM 的 UTF-8" : 




這時用上面同樣的程式去讀取就會看到這個 BOM 字元 \ufeff :

>>> with open('file_with_bom.txt', 'r', encoding='utf-8') as f:     
    doc=f.readlines()   
    print(doc)    
    len(doc[0])
    
['\ufeffHello World! \n', '你是在說哈囉嗎?']   
15 
  
這表示若寫入檔案時選擇要添加 BOM 字元, 讀取時卻用 'utf-8' 編碼, 則會顯示此 BOM 字元. 若要去除此 BOM 字元, 編碼須用 'utf-8-sig', 例如 : 

>>> with open('file_with_bom.txt', 'r', encoding='utf-8-sig') as f:    
    doc=f.readlines()    
    print(doc)   
    len(doc[0])    
    
['Hello World! \n', '你是在說哈囉嗎?']
14  

這樣讀取時就會將 BOM 字元去除, 用 len() 檢查可知長度已經少了一個字元, 變成 14 了. 

沒有留言 :