純文字(帶 BOM 的 UTF-8)檔案格式
以 UTF-8 編碼、帶位元組順序標記的純文字。
- MIME 類型
- text/plain
- 副檔名
- .txt
- 分類
- 文字與標記語言
Magic number(檔案簽章)
| 0x00000000 | EFBBBF | ··· | UTF-8 BOM |
關於純文字(帶 BOM 的 UTF-8)格式
位元組順序標記就是放在檔案最前面的字元 U+FEFF。在 UTF-16 裡它名副其實,因為 FF FE 和 FE FF 分得出來,讀取端也真的需要被告知這個檔案是哪個方向。UTF-8 根本沒有位元組順序可標:EF BB BF 這三個位元組純粹是用來宣告「這個檔案是 UTF-8」的訊號,而 Unicode 標準既不要求也不建議加它。Windows 的編輯器還是照加,因為在 Windows 上不加的下場是檔案被當成系統字碼頁去讀——結果就是 Windows 與 Unix 工具之間一道長年的低度摩擦,撐過了每一次想要一勞永逸解決它的嘗試。
檔頭結構,一個位元組一個位元組看
| 位移 | 長度 | 欄位 | 意義 |
|---|---|---|---|
| 0 | 3 | UTF-8 byte-order mark | EF BB BF,是 U+FEFF 的 UTF-8 編碼,那個字元的 Unicode 名稱是 zero width no-break space。它前面不能有任何東西:不在位移 0 的標記根本不是標記,只是一個卡在文字中間的隱形字元。 |
| 0 | 1 | Lead byte EF | 二進位是 1110 1111。最高三個位元標示這是三位元組序列的開頭,這也正是這個標記在 UTF-8 要花三個位元組、在 UTF-16 只要兩個的原因——字元本身沒變,只是編碼長了一點。 |
| 1 | 2 | Continuation bytes BB BF | 10 111011 和 10 111111。把每個的最高兩位元剝掉,剩下的接到開頭位元組那 4 個資料位元後面,就得到 1111 1110 1111 1111——那就是 U+FEFF。親手算一次,是讓你不再把這個標記當成隨便一個 magic number 的最快方法。 |
| 3 | variable | Start of the text | 這個檔案真正的用途從這裡開始。shebang 那一行、XML 宣告、JSON 的左大括號、CSV 的標頭列、PHP 的開始標籤:全都晚了三個位元組,而這個格式造成的每一個問題,都是這一件事的後果。 |
| 3 | 3 | A second byte-order mark | EF BB BF EF BB BF 常見到值得刻意檢查一下。它發生在「會加標記的編輯器存了一個已經有標記的檔案」的時候,也發生在檔案被串接的時候——後者會讓第二個檔案的標記埋在結果的中間,而那裡永遠不會有東西把它顯示給你看。 |
怎麼跟長得像的格式分開
| 容易搞混的 | 怎麼分 |
|---|---|
| XML | 這兩者根本不是二選一——標記是前綴,底下的檔案還是原本那個東西。改變的是這個格式自己的辨識流程得先跳過三個位元組。XML 是少數正面處理這件事的標準之一:它允許宣告前面有一個位元組順序標記,而且只允許這個,所以那三個位元組合法,一個空白字元卻不合法。 |
| UTF-16 text | 是兩個位元組而不是三個:小端序是 FF FE,大端序是 FE FF。你其實很少需要靠標記分辨它們,因為 UTF-16 文字在 hex dump 裡一眼就認得出來——每個 ASCII 字元旁邊都帶一個 00,所以一份英文文件整條右欄看起來就是字母與零位元組交替。 |
| UTF-32 text | 大端序是 00 00 FE FF,小端序是 FF FE 00 00。小端序那個真的有歧義:同樣這四個位元組也可以是一個 UTF-16 小端序標記後面接一個 NUL 字元,而要在兩種讀法之間選擇,只能看它們後面接的是什麼。 |
| Windows-1252 text | 同樣的位元組被用錯的假設去讀。UTF-8 標記被當成 CP1252 或 Latin-1 解碼時,會在文件開頭變成三個看得見的字元——一個帶分音符的 i、一個右指的雙角括號引號,還有一個倒問號。看到那些代表檔案沒事、是讀的人錯了,跟它看起來的樣子正好相反。 |
常見的壞法
- 標記卡在指令碼前面。核心會找位移 0 的 23 21 來確定直譯器,結果找到 EF BB BF,於是指令碼失敗,跳出的訊息講的是直譯器無效或 exec 格式錯誤,既沒提標記也沒提第一行。而這個檔案在每個編輯器裡看起來都完美無缺。
- 標記出現在被 include 的 PHP 檔裡。那三個位元組落在開始標籤外面,所以算成輸出,於是下一個想設 header 的呼叫就跳 headers already sent,並且怪罪到一個「第一行上面看起來什麼都沒有」的檔案身上。
- 標記出現在 CSV 裡。第一欄的名稱變成「標記加上原本的文字」,所以用標頭名稱查找就會落空,而檔案在畫面上看起來毫無瑕疵。Excel 是刻意寫這個標記的——它就是靠這個認出 UTF-8 CSV——所以這種情況最常出現在「完全照著預期匯出」的檔案裡。
- 標記卡在 JSON 前面。規格說實作不得加它、可以忽略它;Node 和瀏覽器背後的 JavaScript 引擎兩件事都沒做,直接丟出 unexpected token 錯誤,指名一個沒有編輯器會顯示、游標也永遠停不上去的字元。
- 標記跑到中間去了。用 cat、或任何會追加文字的程式碼把檔案串起來,會讓第二個檔案的標記以零寬字元的形式嵌在結果裡。它撐得過 diff、在編輯器裡看不見,然後把一個本該通過的字串比對變成毫無理由地失敗。
這個格式現在還在用嗎
這個標記不會消失,也不會變成常態。Windows 記事本、較舊的 PowerShell 輸出 cmdlet、Visual Studio 和 Excel 到現在都還會寫它,而幾乎所有出身 Unix 的東西,看到帶標記的檔案都覺得它有點壞掉。實務上沉澱下來的規則是:到處都寫不帶標記的 UTF-8,只有一個固定例外——要給 Excel 開的 CSV,那個標記是唯一能可靠阻止 Excel 亂猜系統字碼頁、把檔案裡所有帶重音的名字弄爛的辦法。編輯器會把這個選擇做成兩種不同的編碼,通常叫 UTF-8 和 UTF-8 with BOM,而一個兩種混用的專案,遲早會生出一個「某一行看起來改了、其實沒改」的 diff。最常被忽略的一點是:這個標記是內容,不是中繼資料。Git 會存它、雜湊涵蓋它、位元組數包含它,而在任何工具的任何量測方式下,帶標記的檔案和不帶標記的檔案就是兩個不同的檔案。
在這個站上打開純文字(帶 BOM 的 UTF-8)檔
這些全部在你的瀏覽器裡跑,檔案不會離開你的電腦。
相關格式
容易和純文字(帶 BOM 的 UTF-8)搞混的格式,可以比對它們的開頭位元組。