HTML ファイル形式
HyperText Markup Language。ウェブページの構造を記述する形式。
- MIME タイプ
- text/html
- 拡張子
- .html, .htm
- カテゴリ
- テキスト・マークアップ
マジックナンバー(ファイル署名)
'<!doctype html' または '<html' で始まるプレーンテキスト。
HTML 形式について
HTML を定義しているのは文法ではなくパーサーです。標準は「合法な文書の集合を示して残りを拒否する」という書き方をしていません。あらゆる入力に対してブラウザが何をしなければならないかを、バイト単位で記述しています。壊れた入力も、途中で切れた入力も、そもそも HTML ですらない入力も含めてです。だから実務上「不正な HTML ファイル」というものは存在しませんし、まったく同じ壊れ方のマークアップから 2 つのブラウザが同一のツリーを組み上げますし、パースを始める前にツールが確認できるヘッダも長さもバージョンもトレーラーも、この形式には 1 つもありません。
ヘッダーの構造(バイト単位)
| オフセット | 長さ | フィールド | 意味 |
|---|---|---|---|
| 0 | 3 | UTF-8 byte-order mark | EF BB BF。これがあるとブラウザはそこで探すのをやめます。標準は「BOM があればエンコーディングは確定」と定めていて、それは Content-Type ヘッダの charset も、文書中の meta 要素も上書きします。HTML において固定バイトオフセットで決まることの、数少ない 1 つです。 |
| 0 | 15 | DOCTYPE | 3C 21 44 4F 43 54 59 50 45 20 68 74 6D 6C 3E、今どきの doctype の 15 バイトです。大文字小文字は問いませんし、ブラウザの sniffing 規則が要求するのは最初の 14 バイト(HTML までの文字)と、その後ろに続く空白か閉じ山括弧だけです。 |
| 0 | 5 | html start tag | 3C 68 74 6D 6C に、空白か閉じ山括弧が続いたもの。doctype は必須ではないので、これがオフセット 0 に来ても完全に合法です。ただし doctype の無い文書は quirks モードでパースされ、数十年前のレイアウト挙動が戻ってきて、ボックスの寸法計算が設計時のものと合わなくなります。 |
| 0 | variable | Any other recognised opener | sniffing の表は HEAD、SCRIPT、IFRAME、H1、DIV、FONT、TABLE、A、STYLE、TITLE、B、BODY、BR、P とコメントの開始にも一致します。いずれも先頭の空白は許され、名前の後ろに空白か閉じ山括弧が必要です。ページから切り出して保存した断片は、上 2 行のどちらでもなく、たいていこれらのどれかに一致します。 |
| 0 | 1024 | Encoding prescan window | フィールドではありませんが、頭に入れておく価値のある境界です。ブラウザは meta の charset を先頭 1024 バイトの中でしか探さないことになっています。長いライセンスのコメントやインライン化したスクリプトのせいでそれより後ろへ押し出された charset 宣言は、単に見てもらえず、ページは推測にフォールバックします。 |
似た形式との見分け方
| 紛らわしい形式 | 見分け方 |
|---|---|
| XML | 3C 3F 78 6D で始まる文書は XML であり、application/xhtml+xml として配信された XHTML は XML のパーサーへ行き、そこでは閉じ忘れたタグ 1 つが致命傷になります。まったく同じバイト列が text/html として配信されれば、黙って復旧されて普通に描画されます。違いはファイルの中にはまったくありません。サーバーが送ったヘッダの中にあります。同じディレクトリにある同じ文書が、まるで違う 2 つの振る舞いをし得るのはそのためです。 |
| SVG | 単体の .svg は svg をルート要素に持つ XML で、XML のパーサーが処理します。同じマークアップを HTML 文書の中にインラインで書くと、HTML のパーサーが foreign content の規則で処理し、タグ名の大小文字は補正され、自己閉じ構文も特別扱いされます。要素のバイト列は同一であり得て、意味を決めるのはどちらのパーサーがそれを見るかです。 |
| Plain text/Markdown | 確実なバイトの検査は存在しません。どのタグのパターンにも一致しなければ、先頭にバイナリが含まれていない限り sniffing のアルゴリズムはプレーンテキストにフォールバックします。だから README も、変数で始まるテンプレートも、メールの本文も、バイトレベルでは散文と区別が付きません。手掛かりは拡張子と Content-Type だけで、どちらも証拠ではなく主張です。 |
| Failed downloads | hex ビューアで HTML について知っておくと一番役立つのは、ダウンロードの失敗の正体がたいていこれだということです。開けない ZIP、読み込めない画像、リーダーに拒否される PDF。開いてオフセット 0 に 3C 21 44 4F 43 54 59 50 45 があれば、見ているのはログインページか、レート制限の通知か、欲しかったファイルの名前で保存された 404 です。 |
よくある壊れ方
- doctype の前に何かがある。空白とコメントは無視されて何のコストもありませんが、実際のテキストが前にあると(サーバーサイド言語が出した警告、include したファイルから紛れ込んだ 1 行)、文書は quirks モードへ落ちます。それでもページは描画されます。常に描画されるからです。ただしボックスの寸法計算や継承されるフォントサイズが、検証したレイアウトと微妙に違います。
- エンコーディングがどこにも宣言されていない。BOM も無く、Content-Type にも charset が無く、先頭 1024 バイトに meta も無ければ、ブラウザは推測しますし、その推測は読み手のロケールに左右されます。作った職場では正しく見えて、別のどこかでは文字化けだらけになります。バイトはどの時点でも変わっていないのに。
- 途中切れ。中途半端なダウンロードには手掛かりがまったくありません。パーサーは入力が尽きた時点で開いている要素をすべて閉じ、手元にあるものを描画します。フッターの無いページは、途中で止まったファイルというより、テンプレートのバグに見えます。確かめる唯一の方法は、バイト数を告知された Content-Length と比べることです。
- 二重エスケープ。テンプレートを通る途中で 2 回エスケープされたテキストは、書式ではなくタグの構文がそのまま見える形で描画されます。しかもその間ファイルは完全に整形式のままです。探すべきバイト列は 26 61 6D 70 3B、つまりエスケープされたアンパサンドが、エスケープされた山括弧の前に居座っている状態です。
- 保存したページは、そのページではない。名前を付けて保存は URL を書き換え、リソースを移動またはインライン化し、出どころを記録するコメントを差し込みます。だからディスク上のバイトは、サーバーが送ったバイトではありません。保存したページと取り直したものを diff しても、報告されるのはほとんどブラウザ自身の編集なので、保存した控えは証拠としてほぼ役に立ちません。
この形式の現在地
HTML が置き換えられることはないので、面白い問いは「ブラウザの外のどこに出てくるか」のほうです。最大の場所はメールで、しかも化石のような部分集合(テーブルレイアウトとインラインスタイル)で動いています。メールクライアントはパースの標準を採用しませんでしたし、これからもしません。Electron とその模倣品はデスクトップアプリの中にブラウザエンジンを入れたので、人が毎日使うソフトウェアのかなりの割合は、表面が HTML です。静的サイトジェネレータ、ドキュメントツール、デザインシステムはどれも、人が手で書くものとしてではなくビルドの成果物として HTML を吐きます。そしてディスク上のファイルとしては、たいてい事故で出会います。別の形式のはずのものを開いたら、サーバーがエラーページを返していた。そのときに見つかるのが HTML です。先頭 15 バイトを覚えておくことが、ファイルを扱う人にとってかなり役に立つ知識である理由がこれです。
HTML ファイルをこのサイトで開く
どれもブラウザ内で動きます。ファイルが端末から出ることはありません。
関連する形式
HTML と混同しやすい形式です。先頭バイトを見比べてください。