ByteScope

HTML 檔案格式

HyperText Markup Language,網頁的結構格式。

MIME 類型
text/html
副檔名
.html, .htm
分類
文字與標記語言

Magic number(檔案簽章)

以 '<!doctype html' 或 '<html' 開頭的純文字。

關於 HTML 格式

HTML 是由它的解析器定義的,不是由文法定義的。標準並沒有描述一組合法文件、然後把其他東西全部拒收;它是逐位元組描述瀏覽器面對任何輸入時該做什麼,包括格式錯誤的、被截斷的、甚至根本不是 HTML 的輸入。這就是為什麼實務上根本不存在「無效的 HTML 檔案」,為什麼兩個不同的瀏覽器面對同樣壞掉的標記會建出一模一樣的樹,也為什麼這個格式沒有標頭、沒有長度、沒有版本、也沒有尾標可以讓工具在開始解析之前先檢查。

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

位移長度欄位意義
03UTF-8 byte-order markEF BB BF。它一出現,瀏覽器就不再往下找了:標準規定標記讓編碼成為確定的事實,優先於 Content-Type 標頭裡的 charset,也優先於文件裡任何 meta 元素。這是 HTML 裡極少數在固定位元組位移上就定案的東西。
015DOCTYPE3C 21 44 4F 43 54 59 50 45 20 68 74 6D 6C 3E,現代 doctype 的 15 個位元組。大小寫無所謂,而瀏覽器的嗅探規則只要求前 14 個——到 HTML 這幾個字母為止——後面接一個空白或右角括號就算數。
05html start tag3C 68 74 6D 6C,後面再接一個空白或右角括號。放在位移 0 完全合法,因為 doctype 不是必要的;沒有 doctype 的文件會用 quirks 模式解析,一堆幾十年前的排版行為回來了,而 box sizing 也不再是當初設計的那樣。
0variableAny other recognised opener嗅探表還會比對 HEAD、SCRIPT、IFRAME、H1、DIV、FONT、TABLE、A、STYLE、TITLE、B、BODY、BR、P 以及註解開頭,每一個都允許前面有空白,而名稱後面都必須接空白或右角括號。從頁面裡存下來的片段,通常命中的是這些,而不是前面兩列的那兩種。
01024Encoding prescan window這不是一個欄位,而是一條值得記在腦子裡的界線:規範要求瀏覽器只在前 1024 個位元組裡找 meta charset。被一大段授權註解或行內指令碼推到那之後的 charset 宣告,等於根本沒被看見,頁面就退回去用猜的。

怎麼跟長得像的格式分開

容易搞混的怎麼分
XML開頭是 3C 3F 78 6D 的文件是 XML,而以 application/xhtml+xml 送出的 XHTML 會進 XML 解析器,那裡少關一個標籤就是致命的。同樣的位元組改用 text/html 送出,會被無聲地修復、正常算繪。差別根本不在檔案裡,而在伺服器送的標頭裡——這也是為什麼同一份文件放在同一個目錄下,行為可以完全是兩回事。
SVG獨立的 .svg 是帶著 svg 根元素的 XML,由 XML 解析器處理。同樣的標記寫在 HTML 文件裡面時,則是由 HTML 解析器依 foreign content 規則處理,標籤名稱會被修正大小寫、自我關閉語法也有特殊處理。這些元素可以逐位元組相同;決定它們是什麼意思的,是哪個解析器看到它們。
Plain text/Markdown沒有可靠的位元組測試。如果所有標籤樣式都沒命中,只要開頭那些位元組不含二進位資料,嗅探演算法就退回純文字,所以一份 README、一個以變數開頭的樣板,或一封信的內文,在位元組層級和一般散文分不出來。你手上只有副檔名與 Content-Type,而兩者都是宣稱、不是證據。
Failed downloads拿 hex 檢視器看 HTML 最有用的一件事是:壞掉的下載最後往往就是它。打不開的 ZIP、載不出來的圖片、閱讀器拒收的 PDF——打開一看,位移 0 是 3C 21 44 4F 43 54 59 50 45,那你看的就是一頁登入畫面、一則流量限制通知,或是一個 404,只是被存成了你原本想要的那個檔名。

常見的壞法

  • doctype 前面有東西。空白與註解會被忽略、沒有代價,但只要前面有真正的文字——伺服器端語言吐出來的警告、被 include 的檔案漏出來的一行——文件就掉進 quirks 模式。頁面照樣算繪,因為它永遠都會算繪,只是 box sizing 和繼承來的字級和當初測過的版面有微妙的差異。
  • 哪裡都沒有宣告編碼。沒有標記、Content-Type 沒有 charset、前 1024 個位元組裡也沒有 meta,瀏覽器就用猜的,而猜法取決於讀者的地區設定。同一頁在做它的辦公室裡是對的,換個地方就滿是亂碼,而位元組從頭到尾一個都沒變。
  • 被截斷。下載一半完全沒有跡象:輸入結束時,解析器把還開著的元素通通關掉,然後把手上的東西算繪出來。少了頁尾的頁面看起來像樣板有 bug,而不是像一個提早結束的檔案,唯一能確定的辦法是把位元組數和對方宣告的 Content-Length 比一比。
  • 重複逸出。文字在通過樣板時被逸出了兩次,算繪出來就是看得見的標籤語法而不是格式,而這期間檔案完全格式良好。要搜尋的位元組是 26 61 6D 70 3B——一個被逸出的 & 符號——卡在一個被逸出的角括號前面。
  • 存下來的頁面不是那個頁面。另存新檔會改寫網址、搬移或內嵌資源,還會注入一段記錄來源的註解,所以磁碟上的位元組不是伺服器送出的位元組。拿存下來的頁面和重新抓一次的結果做 diff,報出來的多半是瀏覽器自己的改動,這也讓存檔副本幾乎沒有證據價值。

這個格式現在還在用嗎

HTML 不會被取代,所以關於它比較有意思的問題是:它在瀏覽器之外會出現在哪。最大宗是電子郵件,而那裡跑的是化石級的子集——表格排版加行內樣式——因為郵件軟體從來沒有採用那套解析標準,以後也不會。Electron 和它的模仿者把瀏覽器引擎塞進桌面應用程式,意思是大家每天在用的軟體有很大一部分,表面上就是 HTML。靜態網站產生器、文件工具與設計系統全都把它當成建置產物吐出來,而不是還有人在手寫。至於磁碟上的檔案,最常是意外遇上它的:你打開一個本該是別種格式的東西,結果伺服器送來的是錯誤頁,那就是 HTML——所以認得出它前十五個位元組,是跟檔案打交道的人最值得背起來的東西之一。

在這個站上打開 HTML 檔

這些全部在你的瀏覽器裡跑,檔案不會離開你的電腦。

相關格式

容易和 HTML 搞混的格式,可以比對它們的開頭位元組。

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

常見問題

檔案的 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 仍然可能夾帶惡意內容。辨識真實類型是有用的第一道檢查,尤其在副檔名和位元組不一致的時候,但它不是惡意軟體掃描。

手上有檔案可能是 HTML?用檔案類型辨識工具查出來 — 它在本機讀取 magic number,不上傳任何東西。或者瀏覽所有格式。