Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Webエンジニアなのにブラウザの仕組みがわからないので、Pythonで自作してみた

Avatar for Tatsuki Tatsuki
August 20, 2026

 Webエンジニアなのにブラウザの仕組みがわからないので、Pythonで自作してみた

2026/08/21のPyCon JP 2026にて登壇した資料になります。

https://2026.pycon.jp/ja/talks/SVZGAA

Avatar for Tatsuki

Tatsuki

August 20, 2026

More Decks by Tatsuki

Other Decks in Programming

Transcript

  1. A 3

  2. B 4

  3. 自己紹介 佐藤 樹 Tatsuki Satoh Recustomer, Inc. / Engineering Manager

    Web エンジニアとしてキャリアを積んできた 普段は Python / TypeScript 9
  4. 対象聴者 想定している方 説明しないこと 普段ブラウザを触っているが、 内部の作りは詳しくない方 何かを自作してみたい方 Python の詳細な構文 HTML /

    CSS / JavaScript そのもの サンプルコードは、Pythonの経験が少ない方でも理解できるような内容にしております。 10
  5. 発表の流れ 1 2 3 4 5 6 7 ブラウザの定義と構成要素 HTTP

    HTML → DOM CSS レイアウトと描画 JavaScript スレッドとロック 11
  6. ブラウザの作り方は、全部文書になっている 誰が書いた 文書 決めていること IETF RFC 9110 / RFC 3986

    ページを取ってくるところ WHATWG HTML Standard HTML を解釈するところ W3C CSS 2.1 / CSS Syntax Level 3 CSS を解釈して描くところ どれも無料で読める。このあと出てくる仕様の引用は、全部この 3 つのどれか。 13
  7. ブラウザは「user agent」の1つ The term "user agent" refers to any of

    the various client programs that initiate a request. 訳 「user agent」という用語は、リクエストを開始するさまざまなクライアントプログラムを指す RFC 9110 (HTTP Semantics) §3.5 User Agents rfc-editor.org/rfc/rfc9110.html#name-user-agents A user agent is any program that interprets a document written in the document language and applies associated style sheets … 訳 user agent とは、文書言語で書かれた文書を解釈し、対応するスタイルシートを適用するあらゆるプログ ラムである CSS 2.1 §3.1 Definitions w3.org/TR/CSS21/conform.html#user-agent HTTP は「取ってくる人」、CSS は「解釈して描く人」として、同じ言葉を定義している。 14
  8. user agent = ユーザーの代理人 取ってきて — RFC 9110「リクエストを開始する」 解釈して・描いて —

    CSS 2.1「文書を解釈し、スタイルシートを適用する」 操作を受け付ける — HTML Standard の適合クラスの名前そのもの Web browsers and other interactive user agents 訳 Web ブラウザ、および対話的に操作できるその他の user agent HTML Standard §2.1.8 Conformance classes html.spec.whatwg.org/multipage/infrastructure.html#conformance-classes 15
  9. user agent は、ブラウザだけじゃない 適合クラス Web browsers and other interactive user

    agents Non-interactive presentation user agents Visual user agents that support the suggested default rendering User agents with no scripting support Conformance checkers Data mining tools Authoring tools and markup generators どういうものか 描いて、人間の操作を受け付ける 描くが操作させない。仕様の例はプリンタと電光掲示板 既定の見た目(UA スタイルシート)を実装する スクリプトを実行しない。JS を切ったブラウザもここ 適合性を検査する。いわゆる HTML validator 描画も検査もせず、中身だけ使う。クローラ HTML を書き出す側。エディタや SSG HTML Standard §2.1.8 Conformance classes html.spec.whatwg.org/multipage/infrastructure.html#conformance-classes 16
  10. httpx を使ってページ情報を取得する self._client = httpx.Client( http2=True, # ← HTTP/2 がこれだけで有効になる

    follow_redirects=True, timeout=self._timeout_seconds, headers={ "User-Agent": self._user_agent, "Accept": DEFAULT_ACCEPT, "Accept-Language": DEFAULT_ACCEPT_LANGUAGE, "Accept-Encoding": DEFAULT_ACCEPT_ENCODING, # brotli / gzip }, limits=httpx.Limits(max_connections=16, max_keepalive_connections=8), ) TLS も HTTP/2 も brotli もコネクションプールも、自分で書かなくていい。 22
  11. URL の解決について HTML に書いてある ../style.css は、 いま開いているページを基準にして、初めて絶対 URL になる。 This

    section describes an algorithm for converting a URI reference that might be relative to a given base URI into the parsed components of the reference's target. 訳 本節では、ある基準 URI に対して相対的でありうる URI 参照を、その参照先の構成要素へと変換するアル ゴリズムを述べる RFC 3986 (URI: Generic Syntax) §5.2 Relative Resolution rfc-editor.org/rfc/rfc3986.html#section-5.2 仕様の側が「アルゴリズム」と言い切っている。 場合分けの手順まで決まっている。 23
  12. 取りに行くもの全部を、この手順に通す も <script src> も <img src> も、取りに行く前に絶対 URL へ直す。

    いま https://docs.python.org/3/library/re.html を開いているとして、 <link href> HTML に書かれている値 実際に取りに行く先 images/logo.png https://docs.python.org/3/library/images/logo.png ../style.css https://docs.python.org/3/style.css /assets/app.js https://docs.python.org/assets/app.js //cdn.jsdelivr.net/npm/app.js — ホスト直下から https://cdn.jsdelivr.net/npm/app.js — スキームだけ引き継ぐ グレー = いま開いているページ由来 / 紫 = HTML に書かれていた値 24
  13. HTML パーサは「2 段のステートマシン」 HTML ⽂字列 HTML パーサ 1 トークナイザ 字句解析

    1 ⽂字ずつ読んで、タグ境界でトークンを放出 StartTag / EndTag / Characters / EndOfFile 2 ツリー構築 構⽂解析 「⽂書のどこにいるか」で挿⼊先を決める DOM ツリー この 2 段は、実装の都合ではなく仕様の指定。 … passed through a tokenization stage followed by a tree construction stage. HTML Standard §13.2.1 html.spec.whatwg.org/multipage/parsing.html#overview-of-the-parsing-model 29
  14. トークナイザ: ステートマシンの仕様書通りに解析 Implementations must act as if they used the

    following state machine to tokenize HTML. The state machine must start in the data state. 訳 実装は、HTML をトークン化するのに以下の状態機械を使ったかのように振る舞わなければならない。状態 機械は data state から開始しなければならない。 HTML Standard §13.2.5 Tokenization html.spec.whatwg.org/multipage/parsing.html#tokenization 13.2.5.1 13.2.5.6 13.2.5.32 13.2.5.36 13.2.5.53 ⋮ 13.2.5.84 Data state Tag open state Before attribute name state Attribute value (double-quoted) state DOCTYPE state Numeric character reference end state 状態は 84 個。ひとつずつ名前が付いていて、どの文字でどこへ移るかまで決まっている。 30
  15. 例: <p class="x"> 84 状態のうち、 <p class="x"> を読むのに通る 8 つ。矢印の文字が遷移の条件。

    > なら、属性なしでそのまま放出 英字 < Data state > Tag open state 空⽩ Before attribute Tag name state name state 英字 StartTag を放出 値の終わり 値の始まり " " = After attribute value Attribute value Before attribute (quoted) state (double-quoted) state value state Attribute name state 31
  16. 状態は、仕様書の名前がそのまま Enum になる class TokenizerState(Enum): DATA = auto() TAG_OPEN =

    auto() END_TAG_OPEN = auto() TAG_NAME = auto() BEFORE_ATTRIBUTE_NAME = auto() ATTRIBUTE_NAME = auto() AFTER_ATTRIBUTE_NAME = auto() BEFORE_ATTRIBUTE_VALUE = auto() ATTRIBUTE_VALUE_DOUBLE_QUOTED = auto() ATTRIBUTE_VALUE_SINGLE_QUOTED = auto() ATTRIBUTE_VALUE_UNQUOTED = auto() AFTER_ATTRIBUTE_VALUE_QUOTED = auto() SELF_CLOSING_START_TAG = auto() # 他の state も定義する 32
  17. 1 状態 = 1 メソッド def tag_name_state(self) -> None: character

    = self._consume() if character is None: self._emit_eof() elif character in WHITESPACE: self.state = TokenizerState.BEFORE_ATTRIBUTE_NAME elif character == "/": self.state = TokenizerState.SELF_CLOSING_START_TAG elif character == ">": self._emit_tag() # ← タグが確定して放出 else: self._tag_name += character.lower() 仕様の「Tag name state」1 節の分岐が、そのままこの 12 行になる。 33
  18. ここからは、2 段目の「ツリー構築」 HTML ⽂字列 HTML パーサ 1 トークナイザ 字句解析 1

    ⽂字ずつ読んで、タグ境界でトークンを放出 StartTag / EndTag / Characters / EndOfFile 2 ツリー構築 構⽂解析 「⽂書のどこにいるか」で挿⼊先を決める DOM ツリー 34
  19. ツリー構築の状態が insertion mode class InsertionMode(Enum): INITIAL = auto() BEFORE_HTML =

    auto() BEFORE_HEAD = auto() IN_HEAD = auto() AFTER_HEAD = auto() IN_BODY = auto() TEXT = auto() AFTER_BODY = auto() AFTER_AFTER_BODY = auto() トークナイザの TokenizerState にあたるもの。 「文書のどこまで読んだか」を表す状態で、実装したのは 9 つ。 35
  20. ツリー構築:「文書のどこにいるか」で挿入先が決まる 実装した 9 つの insertion mode のうち、 <html> から </html>

    まで読むのに通る 8 つ。矢印のタグが 遷移の条件で、紫の 2 つが、読んだ要素の入る先。 <html> <head> <!DOCTYPE html> INITIAL を書かなくても、勝⼿に作られて同じ道を通る <html> BEFORE_HTML <head> IN_HEAD BEFORE_HEAD <head> の中へ挿⼊ </head> </html> AFTER_AFTER_BODY EOF ─ DOM ができあがる </body> AFTER_BODY <body> IN_BODY <body> の中へ挿⼊ AFTER_HEAD 36
  21. 2 つの状態機械が会話して、HTML の解析が進む トークナイザ ツリー構築 state = DATA mode =

    IN_BODY StartTag("script") トークンを渡す mode = TEXT 戻り先を original_mode に退避 「</script> まで、タグとして読むな」 逆向きは、この 1 本だけ switch_to_rawtext("script") state = RAWTEXT Characters("if (a < b) { …") この < は、もうタグの始まりとして読まれない <script> トークンを渡す の中の if (a < b) が壊れないのは、ツリー構築側がトークナイザの状態を切り替えているから。 37
  22. 閉じタグの書き忘れは、直し方まで仕様に書いてある If the stack of open elements has a p

    element in button scope, then close a p element. Insert an HTML element for the token. 訳 開いている要素のスタックに p があれば、その p を閉じる。そのうえで、このトークンの要素を挿入する。 HTML Standard §13.2.6.4.7 The "in body" insertion mode html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody 壊れた HTML をどう直すかは、実装者の判断ではない。だから自作でも Chrome と同じ木になる。 39
  23. CSS も、やることは HTML と同じ CSS ⽂字列 CSS パーサ 1 トークナイザ

    字句解析 1 ⽂字ずつ読んで、識別⼦や記号のトークンを放出 IdentToken / HashToken / OpenBraceToken / … 2 ルール構築 構⽂解析 セレクタと宣⾔をまとめて 1 ルールにする スタイルルール 同じ一文が、CSS の仕様書にも記載がある。 … passed through a tokenization stage followed by a tree construction stage. HTML Standard §13.2.1 / CSS Syntax Module Level 3 §3.1 w3.org/TR/css-syntax-3/#parsing-overview 43
  24. CSS 1 行が、Rule というデータになる .article-card { padding: 16px; border-radius: 8px

    } ↓ StyleRule( selector_text=".article-card", declarations=( Declaration(name="padding", value_text="16px"), Declaration(name="border-radius", value_text="8px"), ), ) 難しいのはパースではなく、この後の「どれが勝つか」の決定。 44
  25. カスケード = 勝ち残り戦 CSS の "C" は Cascading、段々に流れ落ちる滝のこと。同じ要素の color に、

    複数のルールが別の値を指定してくる。そこからどれを採るか決める工程が、カスケード。 #hero .card { color: blue } .card { color: red } .card { color: green } /* ← 先頭に書いてあるのに、これが勝つ */ /* ← red には勝つ。同じ詳細度なので後勝ち */ 45
  26. どれが勝つかの順番は、CSS の仕様が決めている 1. 詳細度(specificity)— #id > .class > tag 2.

    同じ詳細度なら、後に書かれたほうが勝つ 3. 決まらなければ親から継承する(プロパティによる) 4. それでもなければ初期値 CSS 2.1 §6.4.1 Cascading order(1・2)/ §6.1.1 Specified values(3・4) w3.org/TR/CSS21/cascade.html#cascading-order 46
  27. プロパティごとに書いていたら、コード量がすごいことに 詳細度 → 後勝ち → 継承 → 初期値 の 4

    段は、CSS のすべてのプロパティに要る。 color も width も font-size も、通す判定はまったく同じ 同じ処理を、100 種類以上のプロパティすべてに。 color 用、 width 用、 font-size 用 …… と書き分けていたら終わらない。 47
  28. ジェネリクスで、1 関数にまとめる def value_of[T](self, name: str, parse: Callable[[str], T |

    None], initial: T, inherited: T, is_inherited: bool) -> T: default = inherited if is_inherited else initial for text in self._values.get(name, ()): match text.strip().lower(): case "inherit": return inherited case "initial": return initial case "unset" | "revert": return default parsed = parse(text) if parsed is not None: return parsed return default TypeVar の宣言も Generic[T] の継承も要らない。 [T] と書くだけ。 PEP 695 Type Parameter Syntax(Python 3.12〜) peps.python.org/pep-0695 48
  29. 読めない宣言は、その 1 行だけが捨てられる 実サイトの CSS は、同じプロパティをわざと 2 回書く場合がある .card {

    width: 100px; /* ← 保険. 解釈できないものがあった時に適用させる */ width: calc(100% - env(safe-area-inset-left)); /* ← 解釈できない */ } 仕様の「無視する(ignore)」は、その宣言が無かったことになるという意味。 捨てる範囲はエラーの種類ごとに決まっていて、値のエラーなら宣言 1 つ分 プロパティごと消えるわけではないので、残った width: 100px が採用される CSS 2.1 §4.2 Rules for handling parsing errors w3.org/TR/CSS21/syndata.html#parsing-errors 49
  30. DOM の木は、そのまま描かれるわけじゃない 同じ HTML でも、DOM の木と、実際に描かれる木は形が違う。 書いた HTML <div> ブラウザを

    <span>自作</span> <p style="display: none">まだ内緒</p> </div> DOM の⽊ レイアウトツリー(描かれる⽊) <div> BlockBox ← <div> は 1 つの箱になる InlineBox ← <span> は箱にならない。 中の⽂字が親の⾏に並ぶ レイアウト "ブラウザを" <span> <p> "自作" "まだ内緒" ブラウザを ⾃作 display: none → 右の⽊には来ない 54
  31. display の値で表示方法を分岐させる DOM を上からなぞる for child in element.children: child_style =

    self.resolver.resolve(child, style) if child_style.display == "none": continue # ← 木に入らない if child_style.display in ("block", "flex"): self._flush_run(run, style, box) child_box = self._build_box(child, child_style) box.children.append(child_box) # ← 箱を 1 つ作る else: self._process_children(child, child_style, run, box) # ← 親の行に入れる display は見た目の指定に見えて、木の形そのものを決めている。 55
  32. ページの高さは、全部の箱を 1 つに合体させて決める ⼦が親からはみ出すこともある 親 どの箱も⼊る、いちばん⼩さい 1 枚 1 つに合体

    union 親の下端 畳み込み(fold) はみ出した子 箱 1 つが BoxFragment、その矩形が Rect この下端が、ページの⾼さ document_height これをコードにすると、自分の枠から始めて、子の結果も入る大きさに広げていくだけ。 def deep_overflow(fragment: Fragment) -> Rect: rect = fragment.rect # ← 自分の枠から始めて if isinstance(fragment, BoxFragment): # ← 子を持つのは箱だけ if fragment.style.overflow_y.clips and fragment.style.overflow_x.clips: return rect # ← overflow: hidden なら打ち切る for child in fragment.children: rect = rect.union(deep_overflow(child)) # ← 子孫を 1 つの枠に畳む return rect 56
  33. 木をたどるのも、 yield from で for 文になる def descendant_elements(self) -> Iterator[Element]:

    for child in self.children: if isinstance(child, Element): yield child yield from child.descendant_elements() # ← 子の分は子に任せる for element in document.descendant_elements(): # 木が、ただの for になる ... elements = sum(1 for _ in document.descendant_elements()) 再帰を書くのはこの 5 行だけ。使う側は、ただの for で回せる。 57
  34. ペイントする レイアウト -> 箱がどこにあるか ペイント -> 描画する <div style="background: #f1e5fe;

    border: 2px solid #6c26ee"> Hello <span style="color: #6c26ee">riff</span> </div> [ SolidFill(rect=Rect(8, 8, 1264, 22.4), color=Rgba(241, 229, 254)), BorderPaint(rect=Rect(8, 8, 1264, 22.4), widths=Edges(2, 2, 2, 2)), TextPaint(origin_x=10.0, baseline_y=24.2, text="Hello"), TextPaint(origin_x=49.5, baseline_y=24.2, text="riff"), ] 上から順に 背景を塗る → 枠線を引く → 文字を置く。 この 1 個 1 個が描画命令( DisplayItem )。 58
  35. 描画命令 12 種類を、 match 1 つで分岐させる def draw(self, item: DisplayItem)

    -> None: match item: case SolidFill(): self._draw_solid(item) case GradientFill(): self._draw_gradient(item) case ImageFill(): self._draw_image(item) case BorderPaint(): self._draw_border(item) case BoxShadowPaint(): self._draw_box_shadow(item) case TextPaint(): self._draw_text(item) case PushClip(): self._push_clip(item) case PushOpacity(): self._push_opacity(item) case PopState(): self._pop_state() 描画命令( DisplayItem )は 12 種類の frozen dataclass の Union クラスパターンで分岐すると、case の中では mypy が型を絞ってくれる 59
  36. 命令を 1 つ足したら、書き忘れを mypy が名指しする case PushOpacity(): case PopState(): case

    _ as unreachable: self._push_opacity(item) self._pop_state() assert_never(unreachable) 13 種類目の PushFilter を足して、 draw に case を書き忘れると error: Argument 1 to "assert_never" has incompatible type "PushFilter"; expected "Never" [arg-type] 忘れた型の名前を、実行する前に mypy が言ってくれる。 AI 時代に学ぶ好きなルール・嫌いなルール Linter 編 根本翔多 mypy / ruff のどのルールを選ぶのか、AI 時代の判断軸で。8/22(土)10:30-11:00 / ダリア2 もっと詳しく 2026.pycon.jp/ja/talks/GTFVQZ 60
  37. document は自作する JavaScript エンジン= JavaScript のコードを読んで実行するだけの部品。 エンジンに入っている / Map /

    JSON / Math RegExp / class / try-catch エンジンには無い / window / fetch setTimeout / addEventListener Array document = ECMAScript。JavaScript という言語そのもの = DOM / HTML の仕様。ブラウザが用意する document window などはブラウザが用意するものなので、そこは全部自分で作る必要がある。 63
  38. JavaScript エンジンが提供するものはWrapperとして定義 借りているのは pythonmonkey 経由の SpiderMonkey(Firefox のエンジン)。 Python 側(自作の DOM

    実装) JS 側(自作。エンジンが動かす) self._host = { "createElement": self._bridge.create_element, "setAttribute": self._bridge.set_attribute, } createElement(name) { return wrap(host.createElement(String(name))); } 64
  39. JavaScript エンジンが提供しないものは自作で提供 def host_functions(self) -> dict[str, Callable[..., object]]: return {

    "documentHandle": self.document_handle, "createElementNS": self.create_element_ns, "bodyHandle": self.body_handle, "parentHandle": self.parent_handle, "querySelectorHandle": self.query_selector_handle, "boundingRectJson": self.bounding_rect_json, # ... 全 62 個 } エンジンに無かったもの( document / window / fetch )を、ここで全部足している エンジンで提供されているものと同様にWrapperとして提供する 65
  40. 画像の取得に時間がかかっていた 0s 2s 0.82s HTML + CSS 取得 ネットワーク待ち 4s

    6s 2.31s 4.98s JS 実⾏ 画像 132 枚(逐次) CPU 8.11s 全体の 6 割が、この待ち キャッシュ済みなら 132 枚で 0.1 秒。 つまり CPU ではなくネットワーク待ちがボトルネック。 70
  41. 原因: 各スレッドで同じ画像を取得しに行こうとする 時間 スレッド A 置き場を⾒る → まだ無い スレッド B

    取ってきた 画像の置き場 A が取ってきているあいだ、置き場はずっと空のまま ダウンロードする ネットワーク待ち 置き場に⼊れる 置き場を⾒る ダウンロードする → やっぱり無い {} この置き場が self._entries 同じ画像を、もう⼀度 まだ空っぽ 同じ画像を 2 回 取ってきてしまう {"logo.png": 画像} A が⼊れ終わって、やっと⼊る 置き場を見るのと入れるのは、別々の操作。 そのあいだに別のスレッドが入ると、同じ画像を 2 回取りに行く。 71
  42. ロックには 2 つの役割がある def load(self, url: str) -> DecodedImage |

    None: while True: with self._lock: # ① 置き場を壊さないため if url in self._entries: return self._entries[url] waiting = self._inflight.get(url) if waiting is None: arrival = threading.Event() # ② 二度手間を防ぐため self._inflight[url] = arrival break waiting.wait(_TIMEOUT) # 先客がいるので待つ decoded = self._fetch_and_decode(url) with self._lock: self._entries[url] = decoded self._inflight.pop(url, None) arrival.set() # 待っている全員を起こす return decoded ① 置き場を壊さないため( Lock )と、② 同じ画像を何度も取りに行かないため( Event )。 72
  43. 結果:画像の取得が 9.8 倍速くなった with ThreadPoolExecutor(max_workers=min(workers, len(pending))) as pool: for future

    in [pool.submit(self.load, url) for url in pending]: with contextlib.suppress(Exception): future.result() 4.98s → 0.51s 画像 132 枚 / 9.8 倍 で順序を保ったまま重複除去 ThreadPoolExecutor を with で使い、後始末を任せる dict.fromkeys 2 万文字で語る Python の with 文で始めるリソース管理 curekoshimizu with が何を保証しているのかを、 contextlib / ExitStack / async with から 他言語のリソース管理技法まで横断して書いた 1 本。 もっと詳しく zenn.dev/recustomer/articles/675db47214c2b8 73
  44. Python だからできたこと 使ったもの Enum + 1 状態 1 メソッド match

    + assert_never PEP 695 def value_of[T] frozen dataclass 再帰 + yield from threading / Event skia-python / httpx / pythonmonkey ブラウザのどこで効いたか HTML / CSS のトークナイザがそのまま仕様の写経になる 12 種類の描画命令のディスパッチ。命令を足して書き忘れると mypy が止める CSS 全プロパティのカスケードを 1 関数に集約 カスケードの出力。CSS の初期値を、デフォルト値としてそのまま書ける ツリーを畳む処理が数行で書ける 画像取得 4.98s → 0.51s、二重取得も防ぐ C++ の巨大な部品を pip で連れてきて繋ぐ 外部依存はたった 3 つ。 残りは全部標準の Python で書けた。 75
  45. Recustomer からは、5 名が登壇します 2026年8月21日(金) 11:45 Pythonの実行はどこまで賢くなったのか:CPythonとPyPyから見 る最適化のしくみ 眞鍋 秀悟 |

    ダリア2 16:15 PyO3 で既存 Python 評価器を Rust core にする — wasmbindgen でブラウザにも配るための設計 古川 祐希 | ダリア2 17:00 Webエンジニアなのにブラウザの仕組みがわからないので、Python で自作してみた 本セッション 佐藤 樹 | 会議運営事務室 2026年8月22日(土) 10:30 AI 時代に学ぶ好きなルール・嫌いなルール Linter 編 SPONSOR BOOTH スポンサーブースでも お待ちしております 根本 翔多 | ダリア2 15:45 raiseをやめて得たもの、失ったもの 加藤 雅也 | ダリア2 80
  46. 参考文献 仕様 HTML Standard(WHATWG / Living Standard) html.spec.whatwg.org/multipage CSS Syntax

    Level 3(W3C / 候補勧告ドラフト 2021) w3.org/TR/css-syntax-3 CSS 2.1(W3C 勧告 2011) w3.org/TR/CSS21 RFC 9110(IETF / STD 97, 2022) rfc-editor.org/rfc/rfc9110 RFC 3986(IETF / STD 66, 2005) rfc-editor.org/rfc/rfc3986 書籍・資料 Web Browser Engineering Pavel Panchekha, Chris Harrelson 邦訳『Webブラウザエンジニアリング』 (オライリー・ジャパン 2026) browser.engineering 『[作って学ぶ]ブラウザのしくみ』 土井麻未(技術評論社 2024) gihyo.jp/book/2024/978-4-297-14546-0 「ブラウザ」リクルート エンジニアコース新人研修(2021) speakerdeck.com/recruitengineers 「2 万文字で語る Python の with 文で始めるリソース管理」 curekoshimizu zenn.dev/recustomer/articles/675db47214c2b8 81