【Rust/正規表現と置換】ルビ記法(|《》)をHTMLに自動変換 ~ 正規表現にマッチしたその全てを置換する
Rust で正規表現に適合した箇所を全て置換する術
そもそも世の中にはルビがあり、ルビ(|被ルビ部《ルビ内容》
というものです。その他、幾つもの記法が存在するようですが、ここに枚挙するような遑は御座いませんので参考記事を紹介するに留め置きます。
扨、このようなルビ記法は各小説投稿サイトで用いられているそうですが、このような特殊な記法は、他所では邪魔な記号の混入と成り果ててしまうのが常であります。今回は、比較的どこでも有効な
こうした処理の仕方を調べると、
しかし、今回は
先に結論
これが本記事の全てです。不備などあればお伝えください。
use regex::Regex;
fn replace_all(regex_model: Regex, text: String) -> String {
let replaced = regex_model.replace(
&text,
"<ruby>$1<rp>(</rp><rt>$2</rt><rp>)</rp></ruby>"
);
if regex_model.is_match(&replaced) {
replace_all(regex_model, (&replaced).to_string())
} else {
replaced.to_string()
}
}
fn main() {
let text = "くれなゐのはつ|花《はな》そめのいろふかくおもひし|心《こゝろ》われわすれめや".to_string();
let regex_model = Regex::new(r"\u{007c}(.+?)《(.+?)》").unwrap();
let returned_string = replace_all(regex_model, text);
println!("{}", returned_string);
}
くれなゐのはつ<ruby>花<rp>(</rp><rt>はな</rt><rp>)</rp></ruby>そめのいろふかくおもひし<ruby>心<rp>(</rp><rt>こゝろ</rt><rp>)</rp></ruby>われわすれめや
詳細は後述するとして、概要を図示しましょう。
樊籬は示しました。一つずつ漸進して参りましょう。
HTML によるルビ記法のおさらい
先ずは
こちらを参考にしています。
<ruby>被ルビ部<rp>(</rp><rt>ルビ内容</rt><rp>)</rp></ruby>
タグ名 | 意味 |
---|---|
<ruby></ruby> |
|
<rp></rp> |
括弧を示す。 |
<rt></rt> |
註釈の内容を表す。 |
<rp></rp>
は、一見して存在意義の知り難い要素です。例えば<ruby>被ルビ部<rp>(</rp><rt>ルビ内容</rt><rp>)</rp></ruby>
が何らかの要因でルビを形成しなかった場合、被ルビ部(ルビ内容)
と表示されます。このような想定の元、ルビに()
を付する目的で使用するのが<rp></rp>
です。
ですから、このような配慮を要せぬ場合には、単に<ruby>被ルビ部<rt>ルビ内容</rt></ruby>
で充分であるということが言えます。使用意図に応じて使い分けましょう。
Rust での正規表現
Regex
次の操作をすることで、図のような最低限の要素が自動で構成されます。茲ではkurenawi
の名前でパッケージを生成しました。
cargo new kurenawi
project root
にて次の通り操作することで、Cargo.toml
にRegex
cargo add regex
[dependencies]
regex = "1.9.5"
今回のバージョンは1.9.5
でした。
正規表現を確かめる
「古今和歌集・一四・恋四・七二三」から、一首和歌を引用しましょう。紅の花の色を初恋の心に準えて歌ったものだそうです。
95/165の左面一首目です。
以下は、正規表現に適合したらtrue
と表示するプログラムです。当然乍ら、適合しなければfalse
となります。
use regex::Regex;
fn main() {
let text = "くれなゐのはつ|花《はな》そめのいろふかくおもひし|心《こゝろ》われわすれめや";
let regex_model = Regex::new(r"\u{007c}(.+?)《(.+?)》").unwrap();
println!(
"is match: {}",
regex_model.is_match(text)
);
}
プログラムに関する概説
let text = "~";
"~"
は文字列を示します。text
は宣言する変数の名です。let
で始めることで、text
の名で変数を定めることができ、その内容は"~"
となります。
Regex::new(r"\u{007c}(.+?)《(.+?)》")
Regex::new(r"\u{007c}(.+?)《(.+?)》")
で、正規表現の評価型式を定めています。重要な箇所はr"\u{007c}(.+?)《(.+?)》"
です。
r
はr
を除いたRegex::new("\u{007c}(.+?)《(.+?)》")
では機能しません。"\u{007c}"
が影響を及ぼさないように、
\u{~}
は、16進法で表現される文字コード「
fn main() {
println!("\u{007c}");
}
|
U+007C
は|
を示すため、\u{007c}
を表示すると|
に変換されて出力されます。亦、この例に使用している通り、println!()
は文字列を表示するための記述です。;(セミコロン)
は一文の結びを示します。
本来ならばr"|(.+?)《(.+?)》"
で宜しいと思われますが、|
が悪影響を及ぼしているためか、実際には機能しません。そのため、|
を排除するため致し方なく\u{007c}
の表記を採用しています。
(.+?)
は、.
と+
と?
の三つからなります。
-
.
は、何らかの文字が入ることを示します。 -
+
は、直前の文字、茲では何らかの文字が1回以上連続することを示します。 -
?
は、+?
とすることで、余計なものが入り込まないようにすることができます。
例:a|b《c》d|e《f》g
について
|(.+)《(.+)》
の場合、|(b《c》d|e)《(f)》
が適合します。
|(.+?)《(.+?)》
の場合、|(b)《(c)》
が適合します。
unwrap()
失敗し得る操作に付する関数です。unwrap()
がなければプログラムを実行できないため、仕方なく付しています。明確な意図を以て付しているのではありません。失敗した場合にはプログラムを停止します。ただし実際には、プログラムの停止はサービス全体の停止を意味します。停止されては困る場合、unwrap_or()
のような、失敗を報知しながらもプログラムを継続させる関数を用います。
println!("is match: {}", regex_model.is_match(text))
println!()
には!
が付きます。!
が付くものを!
の付かないものを関数と呼び分けます。
println!("a = {}", a)
とある場合、変数a
の内容をa = {}
の型に当てはめて表示します。
今回はprintln!("is match: {}", regex_model.is_match(text))
とあるため、regex_model.is_match(text)
の結果をis match: {}
の型に当てはめて表示します。
cargo run
でプログラムを実行します。
PS C:\・・・\kurenawi> cargo run
Compiling kurenawi v0.1.0 (C:\・・・\kurenawi)
Finished dev [unoptimized + debuginfo] target(s) in 1.01s
Running `target\debug\kurenawi.exe`
is match: true
これでは正規表現に適合したか否かの外に何もわかりませんが、この真理値は後に用いる重要な情報です。次に、適合した箇所を上述した
正規表現で適合した箇所を置換する
use regex::Regex;
fn main() {
let text = "くれなゐのはつ|花《はな》そめのいろふかくおもひし|心《こゝろ》われわすれめや";
let regex_model = Regex::new(r"\u{007c}(.+?)《(.+?)》").unwrap();
let ruby_html = "<ruby>$1<rp>(</rp><rt>$2</rt><rp>)</rp></ruby>";
println!(
"is match: {}",
regex_model.is_match(text)
);
println!(
"{}",
regex_model.replace(text, ruby_html)
);
}
プログラムに関する概説
"<ruby>$1<rp>(</rp><rt>$2</rt><rp>)</rp></ruby>"
$1
と$2
は、正規表現に対応しています。
regex_model.replace(text, ruby_html)
regex_model
に基づいて、text
をruby_html
で置換します。結果は次の実行結果の通りです。
PS C:\・・・\kurenawi> cargo run
Compiling kurenawi v0.1.0 (C:\・・・\kurenawi)
Finished dev [unoptimized + debuginfo] target(s) in 1.62s
Running `target\debug\kurenawi.exe`
is match: true
くれなゐのはつ<ruby>花<rp>(</rp><rt>はな</rt><rp>)</rp></ruby>そめのいろふかくおもひし|心《こゝろ》われわすれめや
この通り、始めに適合した|花《はな》
のみが<ruby>花<rp>(</rp><rt>はな</rt><rp>)</rp></ruby>
に置換されており、|心《こゝろ》
はそのままになっています。
正規表現で適合した箇所を全て置換する
全てのルビ記号を置換できなければ意味がないので、全て置換し尽くすまで繰り返しましょう。
use regex::Regex;
fn replace_all(regex_model: Regex, text: String) -> String {
let replaced = regex_model.replace(
&text,
"<ruby>$1<rp>(</rp><rt>$2</rt><rp>)</rp></ruby>"
);
if regex_model.is_match(&replaced) {
replace_all(regex_model, (&replaced).to_string())
} else {
replaced.to_string()
}
}
fn main() {
let text = "くれなゐのはつ|花《はな》そめのいろふかくおもひし|心《こゝろ》われわすれめや".to_string();
let regex_model = Regex::new(r"\u{007c}(.+?)《(.+?)》").unwrap();
let returned_string = replace_all(regex_model, text);
println!("{}", returned_string);
}
プログラムに関する概説
"くれなゐのはつ|花《はな》そめのいろふかくおもひし|心《こゝろ》われわすれめや".to_string()
to_string()
関数によってString
型に変換しています。"くれなゐのはつ|花《はな》そめのいろふかくおもひし|心《こゝろ》われわすれめや"
は&str
型です。意図としては、String
型であればうまくいくからというだけです。
型 | 概要 |
---|---|
&str |
そのまま使う場合 |
String |
内容が変わる場合 関数から返却する場合 |
&str
型は、比較的単純な操作に用いられます。基本的には、内容を改竄したり、継ぎ足したりすることには用いられません。その意味ではString
よりも厳格です。
対するString
型は、内容の改変を良しとしています。内容の改竄も継ぎ足しもできます。&str
型は厳格であるため、関数処理の結果ではこちらを用いざるを得ません。実際のところ、&str
ではできなかったことが、String
ならばうまくいったのです。
fn replace_all(regex_model: Regex, text: String) -> String {}
fn
はreplace_all
という名の関数を定めています。
-
regex_model: Regex
Regex
型の値をregex_model
の名で受けるように定める。 -
text: String
String
型の値をtext
の名で受けるように定める。
-> String
は、返却する値がString
型でなければならないことを示します。
regex_model.replace(&text, "<ruby>$1<rp>(</rp><rt>$2</rt><rp>)</rp></ruby>")
&str
からString
に型を変更したため、&text
とあるように&
を付しています。&
がないと実行できません。また、逐一変数を作る手間を省く意図で、直接
if regex_model.is_match(&replaced) {} else {}
if
は続く命題の真理値によって処理を分岐するものです。
正規表現に適合する箇所がある場合、regex_model.is_match(&replaced)
はtrue
となります。無い場合はfalse
となります。
これにより、置換すべきルビ記号が残留しているか否かを判断しています。
❡true
の場合 replace_all(regex_model, (&replaced).to_string())
置換すべき箇所が残っているため、再びreplace_all
関数で置換します。
扱う文字列型は&str
でなくString
であるため、(&replaced).to_string()
とすることで整合しています。
❡false
の場合 replaced.to_string()
置換し尽くしたため、値を返却します。
返却する値はString
でなければならないため、to_string()
関数を付しています。
PS C:\・・・\kurenawi> cargo run
Compiling kurenawi v0.1.0 (C:\・・・\kurenawi)
Finished dev [unoptimized + debuginfo] target(s) in 0.94s
Running `target\debug\kurenawi.exe`
くれなゐのはつ<ruby>花<rp>(</rp><rt>はな</rt><rp>)</rp></ruby>そめのいろふかくおもひし<ruby>心<rp>(</rp><rt>こゝろ</rt><rp>)</rp></ruby>われわすれめや
これで全ての|《》
が
跋
以上が正規表現と置換に関する一例です。とはいえ、エラーに従っていたらできたものに過ぎません。冀わくはこんなものより優れた手法が現れんことを。
増補:regex_modelの定数化
定数化の障壁
以上の例では、逐一regex_model
を引数に渡しているため、粗さが拭えません。願わくは、regex_model
は定数として予め定義しておき、replace_all
関数の引数で盥回しにする手間を省きたいものです。しかし、単純に関数外に定数として定義しようとすると、以下のように却下されます。
use regex::Regex;
const regex_model: Regex = Regex::new(r"\u{007c}(.+?)《(.+?)》").unwrap();
fn replace_all(text: String) -> String {
let replaced = regex_model.replace(
&text,
"<ruby>$1<rp>(</rp><rt>$2</rt><rp>)</rp></ruby>"
);
if regex_model.is_match(&replaced) {
replace_all((&replaced).to_string())
} else {
replaced.to_string()
}
}
fn main() {
let text = "くれなゐのはつ|花《はな》そめのいろふかくおもひし|心《こゝろ》われわすれめや".to_string();
let returned_string = replace_all(text);
println!("{}", returned_string);
}
error[E0015]: cannot call non-const fn `regex::Regex::new` in constants
--> src\main.rs:3:28
|
3 | const regex_model: Regex = Regex::new(r"\u{007c}(.+?)《(.+?)》").unwrap();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: calls in constants are limited to constant functions, tuple structs and tuple variants
error[E0015]: cannot call non-const fn `Result::<regex::Regex, regex::Error>::unwrap` in constants
--> src\main.rs:3:64
|
3 | const regex_model: Regex = Regex::new(r"\u{007c}(.+?)《(.+?)》").unwrap();
| ^^^^^^^^
|
= note: calls in constants are limited to constant functions, tuple structs and tuple variants
解決策
once_cell::sync::Lazy
を使うことで、定数として関数外に定義することができます。
Cargo.toml
にonce_cell
crate を追加する
Cargo.toml
にRegex
project root
でonce_cell
cargo add once_cell
プログラムを改訂する
once_cell::sync::Lazy
を使って、定数を定義します。プログラム全体では、以下の型式に落ち着きました。
use regex::Regex;
use once_cell::sync::Lazy;
const REGEX_MODEL: Lazy<Regex> = Lazy::new(|| Regex::new(r"\u{007c}(.+?)《(.+?)》").unwrap());
fn replace_all(text: String) -> String {
let replaced = (*REGEX_MODEL).replace(
&text,
"<ruby>$1<rp>(</rp><rt>$2</rt><rp>)</rp></ruby>"
);
if (*REGEX_MODEL).is_match(&replaced) {
replace_all((&replaced).to_string())
} else {
replaced.to_string()
}
}
fn main() {
let text = "くれなゐのはつ|花《はな》そめのいろふかくおもひし|心《こゝろ》われわすれめや".to_string();
let returned_string = replace_all(text);
println!("{}", returned_string);
}
変更点の概説
Lazy::new(|| Regex::new(r"\u{007c}(.+?)《(.+?)》").unwrap());
once_cell::sync::Lazy
は遅延初期化(
亦|| Regex::new(r"\u{007c}(.+?)《(.+?)》").unwrap()
は、所謂無名関数の形を取っています。||
が引数部を表し、Regex::new(r"\u{007c}(.+?)《(.+?)》").unwrap()
が関数としての処理を表します。||
の中に何もありませんから、この関数に引数はなく、唯にRegex::new(r"\u{007c}(.+?)《(.+?)》").unwrap()
の実行されるのみとなります。
const REGEX_MODEL: Lazy<Regex> = ・・・
- 定数を定義するときは、可変と不変の二種より一方を選択します。可変ならば
static
、不変ならばconst
を使用するとあります。しかし可変にする必要はないどころか、寧ろ可変では困るため、不変のconst
を用いています。
-
定数には型を指定する必要があります。
Regex::new(r"\u{007c}(.+?)《(.+?)》").unwrap()
の型はRegex
ですから、単にこれをLazy<>
で囲んでLazy<Regex>
とします。 -
以下のように「定数は大文字(
upper )になさい」と誡告されるため、大文字に直しています。分かり易さからくる慣例ですから、それ以上深い意味は御座いません。case
warning: constant `regex_model` should have an upper case name
--> src\main.rs:4:7
|
4 | const regex_model: Lazy<Regex> = Lazy::new(|| Regex::new(r"\u{007c}(.+?)《(.+?)》").unwrap());
| ^^^^^^^^^^^ help: convert the identifier to upper case: `REGEX_MODEL`
|
= note: `#[warn(non_upper_case_globals)]` on by default
(*REGEX_MODEL)
REGEX_MODEL
はLazy<Regex>
型でした。しかし、実際に必要なものはRegex
型です。<>
の中が欲しい時には、参照外し演算子*
を付加します。
PS C:\・・・\kurenawi> cargo run
Compiling memchr v2.6.2
Compiling regex-syntax v0.7.5
Compiling once_cell v1.18.0
Compiling aho-corasick v1.0.5
Compiling regex-automata v0.3.8
Compiling regex v1.9.5
Compiling kurenawi v0.1.0 (C:\・・・\kurenawi)
Finished dev [unoptimized + debuginfo] target(s) in 8.10s
Running `target\debug\kurenawi.exe`
くれなゐのはつ<ruby>花<rp>(</rp><rt>はな</rt><rp>)</rp></ruby>そめのいろふかくおもひし<ruby>心<rp>(</rp><rt>こゝろ</rt><rp>)</rp></ruby>われわすれめや
正常に動作しています。
Discussion