青空文庫記法を解析する Rust パーサ aozora
aozora は青空文庫記法のパーサ。CommonMark や Markdown は扱わず、青空文庫が配布している .txt ファイルに現れる注釈記法だけを対象にする。実装は Rust で、CLI バイナリ・Rust ライブラリ・WASM・C ABI・Python バインディングの 5 種類で配布する。
use aozora::Document;
let source = "|青梅《おうめ》".to_owned();
let doc = Document::new(source);
let tree = doc.parse();
let html: String = tree.to_html();
let canonical: String = tree.serialize();
let diagnostics = tree.diagnostics();
assert_eq!(canonical, "|青梅《おうめ》");
- handbook: https://p4suta.github.io/aozora/
- リポジトリ: https://github.com/P4suta/aozora
- 現在版: v0.3.0、Rust 1.95 以上、ライセンスは Apache-2.0 OR MIT のデュアル
青空文庫記法とは何か
青空文庫 はパブリックドメインの日本文学を電子化して公開しているボランティアの電子図書館で、1997 年から続いている。各作品は Shift_JIS でエンコードされた .txt ファイルに、本文と独自の注釈記法が混在した形で配布される。
注釈記法には次のような種類がある。
- ルビ:
|青梅《おうめ》で「青梅」に「おうめ」と振り仮名を付ける - 傍点:
[#「ここに傍点」に傍点]で強調用の点を打つ - 縦中横:
[#「23」は縦中横]で縦書き中の数字を横向きに組む - 外字:
※[#「魚+師」、第3水準1-94-37]で JIS X 0213 や Unicode 不在字を参照 - 訓点・返り点: 漢文の読み下し用記号
- 字下げコンテナ:
[#ここから2字下げ]… [#ここで字下げ終わり]の範囲指定 - 改ページ・改段: 章区切りを示す
aozora はこれらを全て認識し、構造化された AST と診断情報に変換する。
インストールと使い方
ビルド済みの CLI バイナリは GitHub Releases に Linux x86_64 / macOS arm64 / Windows x86_64 の 3 種類が aozora-vX.Y.Z-<target>.{tar.gz,zip} 形式で置いてある (SHA256SUMS 同梱)。
ソースから入れるなら cargo install:
cargo install --git <https://github.com/P4suta/aozora> --locked aozora-cli
CLI のサブコマンドはこの 4 つを覚えればだいたい用が足りる。
aozora check FILE.txt # 字句解析と診断
aozora fmt --check FILE.txt # parse → serialize の往復チェック
aozora render FILE.txt # HTML を標準出力へ
aozora check -E sjis FILE.txt # 青空文庫が配布する Shift_JIS をそのまま読む
すべて - (またはパス省略) で標準入力から読める。
Rust ライブラリとしての使い方は冒頭のコード例の通り。Document が bumpalo アリーナを所有し、tree がそこから borrow するライフタイム設計になっている。WASM / C ABI / Python から呼ぶ場合は handbook の Bindings 章にそれぞれの最小例がある。
実装の独自な部分
設計判断として目立つのは 3 点。詳細は handbook の Architecture 章にそれぞれ独立した節がある。
Markdown を扱わない。青空文庫記法は CommonMark とも GFM とも違う独自の文法体系で、aozora は青空文庫記法だけに責務を絞っている。Markdown と組み合わせて使いたい場合は別プロジェクトの afm を使う (afm は aozora を内部に取り込んだ Markdown 方言)。
SIMD マルチパターンスキャナ。青空文庫の本文は UTF-8 で 1 文字 3 バイトが大半を占め、トリガーバイト (|《》※[] および全角空白) の出現密度は 1KB あたり 1 個程度しかない。この希薄な探索を回すために Intel Hyperscan 由来の Teddy アルゴリズムを採用している。AVX2 環境のコーパス計測で 12 GB/s。AVX2 が無い環境では Hoehrmann 系 DFA に runtime dispatch でフォールバックする (3.5 GB/s)。wasm32 ターゲットは memchr 多重スキャンで 1.2 GB/s。
借用アリーナ AST。1 ドキュメントは数万ノードに展開されるため、Box<Node> を素直に並べると malloc がボトルネックになる。aozora は bumpalo 単一アリーナに全ノードを置き、ドキュメント drop 時に 1 回の Bump::reset で全解放する。青空文庫全作品を回すコーパスベンチで Box<Node> 版比 6.4 倍速、ピーク RSS は 30% 減を測っている。
さらに知りたい場合
- handbook: https://p4suta.github.io/aozora/ — 記法の網羅リファレンス、内部設計 (借用アリーナ、SIMD スキャナ、外字解決)、性能チューニング (PGO、samply、コーパススイープ) まで一通り入っている
- API リファレンス: https://p4suta.github.io/aozora/api/aozora/ — rustdoc を自動デプロイ
- 関連プロジェクト
- P4suta/afm — CommonMark + GFM + 青空文庫記法を統合した Markdown 方言
- P4suta/aozora-tools — フォーマッタ、LSP サーバ、tree-sitter 文法、VS Code 拡張
crates.io への公開は v1.0 API 確定後の予定。それまでは tagged commit に依存する形で利用する。