
2025/12/15 1:26
How I wrote JustHTML, a Python-based HTML5 parser, using coding agents
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
JustHTML は軽量な Python ベースの HTML5 パーサで、現在 公式 html5lib テストスイートを 100 % カバーし、外部依存関係ゼロでシンプルな CSS セレクタクエリ API を提供します。
このプロジェクトは、人間の指導の下で GitHub Copilot(Agent モード)、Claude Sonnet 3.7、および Gemini 3 Pro というコーディングエージェントだけで完全に構築されました。上位レベルの目標を設定したものの、すべてのコードはエージェントが生成しました。開発者はコミット、レビュー、設計修正、およびバージョン管理を担当しました。
開発は Rust のトークナイザクレート(
、690 行) から始まりました。このクレートは html5lib の速度にほぼ一致せず、次に rust_tokenizer
html5ever ロジックの Python ポートも同様に不十分でした。html5lib‑tests リポジトリに対する反復テストで、パス率は < 1 % → 30 % に上がり、タグごとのハンドラへリファクタリングした後、さらに微調整を行い 100 % になりました。性能向上は、未テストコードの削除(ツリービルダーを 786 行から 453 行に縮小)と Gemini 3 Pro を用いたベンチマークにより達成されました。
カスタム HTML ファジングツールで 約 300 万件の Web ページ を生成し、クラッシュなしでパーサを強化しました。最終ライブラリは Python コードが約 3,000 行 で構成され、8,500 件以上のテストが通過しています。
他のパーサと比較すると、JustHTML は完全なテストカバレッジを提供し、lxml はわずか 1 % しか達成せず、html5lib 自体は 88 % に留まります。ライブラリは “justhtml” として公開され、継続的インテグレーション、リリース、ドキュメント、クエリアイプ、およびオープンソースライセンスが揃っています。今後の作業では、更なる最適化や現在のテストスイートを超える機能拡張に焦点を当てる可能性があります。
コーディングエージェントと協働する際の実践的なヒント(明確で測定可能な目標設定、生成コードのレビュー、不適切な変更への反論、バージョン管理の活用、および失敗を学習機会として扱うことなど)は、この成功に不可欠でした。
本文
私は最近 JustHTML をリリースしました。これは Python で書かれた HTML5 パーサーで、html5lib のテストスイートを 100 % 通過し、依存パッケージが一切なく、CSS セレクタ検索 API も備えています。この実装を書き上げる過程で、コーディングエージェントと効果的に協働する方法について多くのことを学びました。
なぜ HTML5?
- テストが豊富なプロジェクト を選ぶことで、エージェントのトレーニングが最適化されます。
- HTML5 の仕様は十分に定義済みであり、
リポジトリには数千ものトークナイザ・ツリービルダー用テストが揃っています。html5lib-tests - 完全なテストスイートを持つことで、エージェント自身が進捗を確認し、失敗箇所を把握して改善に繰り返し取り組めます。
パーサーの構築(反復・再起動・性能向上)
1. ワンショット HTML5 パーサー
まずエージェントに極簡易なワンショットパーサーを書かせました。うまく機能しませんでしたが、出発点にはなりました。
2. html5lib-tests
の接続
html5lib-testsテストを組み込んだ結果、通過率は 1 % 未満 に留まりました。これらのテストは非常に難易度が高く、数千もの境界ケースを含んでいます。
3. 約30 % 覆盖までの反復
リファクタリングとバグ修正を重ねて、徐々にカバー率を 約30 % に引き上げました。
4. タグハンドラベースへの再設計
次にハンドラ中心の構造へ切り替えました:
class TagHandler: """すべてのタグハンドラの基底クラス。""" def handle_start(self, context, token): pass class UnifiedCommentHandler(TagHandler): """全状態でコメントを処理するハンドラ。""" def handle_start(self, context, token): context.insert_comment(token.data)
エージェントはこの設計に合わせてコードをリファクタリングしました。
5. 100 % テストカバレッジの達成
Claude Sonnet 3.7 のような強力なモデルを用いて、最終的に 完全カバー を実現しました。
ベンチマークと性能改善
- ベンチマーク: パーサーは
より約 3 倍遅い。html5lib - Rust トークナイザの書き換え: エージェントがトークナイザを Rust で再実装。速度はほぼ
と同等。生成されたhtml5lib
クレートは約 690 行です。rust_tokenizer - Html5ever の発見: Servo の
は高速かつ正確な Rust 実装でした。html5ever - プロジェクトへの疑問: 「本当に作る必要があるのか?」と自問し、削除しようとも考えました。
ピボット:html5ever のロジックを Python に移植
諦めずに、
html5ever からインスパイアされた純粋 Python 実装を目指しました。初期のカバー率は < 1 % でしたが、同じテストで反復して 100 % に到達。依然として html5lib より遅いままでした。
性能作業
- 簡易プロファイラと 10 万件の人気ページを収集するスクレイパーを構築。
- Gemini 3 Pro を使ってベンチマーク・微調整を実施。その他のモデルでは進展がありませんでした。
def _append_text_chunk(self, chunk, *, ends_with_cr=False): if not chunk: self.ignore_lf = ends_with_cr return if self.ignore_lf: if chunk[0] == "\n": chunk = chunk[1:]
未テストコードの削除
カバレッジ分析で大きな未テスト領域を発見。これら(ツリービルダー 786 行 → 453 行)を削除することで、コードはクリーンになり速度も向上しました。
フュージングでクラッシュ検出
エージェントに HTML ファザ―を書かせました:
def generate_fuzzed_html(): """完全にファズされた HTML ドキュメントを生成。""" parts = [] if random.random() < 0.5: parts.append(fuzz_doctype()) # 要素のランダム混合を生成 num_elements = random.randint(1, 20)
ファザ―によりパーサーはクラッシュし、各失敗を修正して新しいテストとして追加。300 万件以上のページ生成後、クラッシュは残っていません。
他パーサーとの比較
- 90 % 以上通過する他パーサー: 存在しない。
(人気 Python パーサー)は 1 % 通過。lxml- 参照実装である
自体が 88 % 通過。html5lib
したがって、100 % カバレッジは稀かつ困難です。
ライブラリとしての公開
エージェントに CI 設定、GitHub でのリリース、クエリ API、ドキュメント作成を任せました。使用例:
from justhtml import JustHTML, query doc = JustHTML("<div><p>Hello</p></div>") elements = query(doc, "div > p")
ライブラリは turbohtml から justhtml に改名されました―速度ではなく、信頼性を示すためです。
エージェントが行ったこと vs. 私の役割
- エージェント: 全コードを書き、面倒な作業を処理し、バグに対して反復。
- 私: 高レベル設計を指導し、コミットをレビューし、誤りを修正。Git のコミットはすべて自分で行い、変更ごとに検証しました。アルゴリズムの詳細までは把握できませんでしたが、間違っている点なら判断できます。
コーディングエージェントとの実務的ヒント
- 明確かつ測定可能な目標 を設定する:「テストを通過させる」方が「コードを改善する」よりも効果的です。
- 変更点のレビュー:生成されたコードを読んで問題点を把握し、学びます。
- フィードバックを惜しまない:何かがおかしいと感じたら遠慮なく伝える(例:「好きではありません」)。
- バージョン管理を活用:エージェントが逸脱した場合は戻せるようにする。
- 失敗を許容:失敗するコマンドを実行させることで、エージェントは学習します。
価値があったか?
はい。JustHTML は約 3,000 行の Python コードで 8,500+ テストに合格しており、私はエージェントなしではそんなに早く完成できませんでした。「迅速」=「考えない」わけではありません。コードレビューや設計判断を行い、エージェントを導きました。作業分担はうまく機能しました:エージェントが入力し、私は思考する。