青空文庫記法を解析する Rust パーサ aozora

Share

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, "|青梅《おうめ》");

青空文庫記法とは何か

青空文庫 はパブリックドメインの日本文学を電子化して公開しているボランティアの電子図書館で、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 ライブラリとしての使い方は冒頭のコード例の通り。Documentbumpalo アリーナを所有し、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 に依存する形で利用する。

Read more

外字と訓点を compile-time hash で解く

aozora は青空文庫の外字参照 (※[#「魚+師」、第3水準1-94-37] のような形) を約 14,000 件のテーブルで解決する。このテーブルを runtime の HashMap ではなく phf (perfect hash function) で持ち、コンパイル時に static 配列に焼き込んでいる。この記事はその選択の根拠と、JIS X 0213 → Unicode フォールバックの設計をまとめたもの。 handbook の対応章: Shift_JIS + 外字 resolver。 外字テーブルの形 外字エントリには 3 種類の解決結果があり、それぞれに対応する variant を GaijiEntry に持たせている。 static GAIJI_TABLE: phf::Map<

By Sakashita Yasunobu

青空文庫の .txt を HTML に変換する最短手順

青空文庫 で配布されている .txt ファイルを HTML に変換したい、という用途向けの手順。Rust の知識は要らない。コマンド 1 行で済む。 1. CLI バイナリを取ってくる aozora の Releases ページ から自分の OS 向けのアーカイブを落とす。 OS アーカイブ名 Linux x86_64 aozora-vX.Y.Z-x86_64-unknown-linux-gnu.tar.gz macOS arm64 aozora-vX.Y.Z-aarch64-apple-darwin.tar.gz Windows x86_64 aozora-vX.Y.Z-x86_64-pc-windows-msvc.zip SHA256SUMS も同梱されているので、

By Sakashita Yasunobu

50,000 ノードの AST を 16 回のアロケーションで: bumpalo 借用アリーナの実例

aozora の AST は bumpalo 単一アリーナの上に構築されている。Box<Node> を素直に並べた版に比べてパースが 6.4 倍速、ピーク RSS が 30% 減という結果が出ている。この記事は、その設計判断と Rust ライフタイムの取り回しを実装の視点から整理したもの。 handbook の対応章: Borrowed-arena AST。 問題設定 青空文庫の典型的な作品は約 500KiB のソースで、aozora がパースすると約 50,000 ノードの木に展開される。素直に Rust らしく書けば次のような形になる。 enum Node { Plain(String), Ruby { target: String, gloss: String }, Container { kind:

By Sakashita Yasunobu