ByteScope

純文字(帶 BOM 的 UTF-8)檔案格式

以 UTF-8 編碼、帶位元組順序標記的純文字。

MIME 類型
text/plain
副檔名
.txt
分類
文字與標記語言

Magic number(檔案簽章)

0x00000000EFBBBF···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 工具之間一道長年的低度摩擦,撐過了每一次想要一勞永逸解決它的嘗試。

檔頭結構,一個位元組一個位元組看

位移長度欄位意義
03UTF-8 byte-order markEF BB BF,是 U+FEFF 的 UTF-8 編碼,那個字元的 Unicode 名稱是 zero width no-break space。它前面不能有任何東西:不在位移 0 的標記根本不是標記,只是一個卡在文字中間的隱形字元。
01Lead byte EF二進位是 1110 1111。最高三個位元標示這是三位元組序列的開頭,這也正是這個標記在 UTF-8 要花三個位元組、在 UTF-16 只要兩個的原因——字元本身沒變,只是編碼長了一點。
12Continuation bytes BB BF10 111011 和 10 111111。把每個的最高兩位元剝掉,剩下的接到開頭位元組那 4 個資料位元後面,就得到 1111 1110 1111 1111——那就是 U+FEFF。親手算一次,是讓你不再把這個標記當成隨便一個 magic number 的最快方法。
3variableStart of the text這個檔案真正的用途從這裡開始。shebang 那一行、XML 宣告、JSON 的左大括號、CSV 的標頭列、PHP 的開始標籤:全都晚了三個位元組,而這個格式造成的每一個問題,都是這一件事的後果。
33A second byte-order markEF 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)搞混的格式,可以比對它們的開頭位元組。

格式開頭位元組副檔名
XML無固定簽章.xml
JSON無固定簽章.json
HTML無固定簽章.html

常見問題

檔案的 magic number 是什麼?

檔案開頭的前幾個位元組,不管檔名寫什麼,它標明的是真正的格式。PNG 一定以 89 50 4E 47 開頭,ZIP 則是 50 4B 03 04。作業系統、瀏覽器和 file 指令讀的都是這幾個位元組,而不是相信副檔名 — 這就是為什麼把檔案改名並不會改變它實際上是什麼。

我要怎麼自己檢查 magic number?

在 macOS 或 Linux 上,xxd -l 16 檔名 會印出前 16 個位元組;在 Windows 上,PowerShell 的 Format-Hex -Path 檔名 -Count 16。或者把檔案丟進本站的檔案類型辨識工具,它會在本機讀取位元組並告訴你是什麼格式,不會上傳任何東西。

為什麼兩種格式的 magic number 會一樣?

因為其中一種是建立在另一種之上。docx、xlsx、pptx、apk、jar 與 epub 骨子裡都是 ZIP 壓縮檔,所以全都以 ZIP 的簽章開頭;要分辨它們得看裡面的項目。Java class 檔和 macOS 的通用二進位檔都以 CA FE BA BE 開頭,靠緊接其後的欄位區分。

有完全沒有 magic number 的檔案嗎?

有。XML、HTML、JSON、SVG、CSV 這類以文字為基礎的格式沒有固定的位元組簽章,所以改用結構來辨識:開頭的 <?xml 宣告、<svg 元素,或是 { 後面接著帶引號的鍵。這種偵測是合理推測而非確定,因此本參考頁會把這些格式標示為結構偵測。

magic number 相符就代表檔案安全嗎?

不代表。它告訴你的是格式,不是內容是否無害 — 一個貨真價實的 PDF 或 docx 仍然可能夾帶惡意內容。辨識真實類型是有用的第一道檢查,尤其在副檔名和位元組不一致的時候,但它不是惡意軟體掃描。

手上有檔案可能是純文字(帶 BOM 的 UTF-8)?用檔案類型辨識工具查出來 — 它在本機讀取 magic number,不上傳任何東西。或者瀏覽所有格式。